Mitosis Labs

Memory

Asking a memory

cortex_ask, cortex_recall, and how to read what comes back.

cortex_ask

The default retrieval call. It fuses vector search, full-text search and graph expansion, then returns the fused result set with provenance attached. No model runs server-side: what comes back is evidence to reason over, not prose.

{
  "question": "What did we decide about pricing last quarter?",
  "limit": 10,
  "since": "2026-04-01T00:00:00Z"
}
ArgumentTypeNotes
questionstringRequired. Natural-language question or search query.
limitnumberMax results. Defaults to 10.
source_tablestringRestrict to one source table, e.g. gmail_messages.
sincestringRFC 3339 lower bound on item time.
untilstringRFC 3339 upper bound on item time.

Follow-up questions are new questions

A result describes only the question it was fetched for. There is no conversational state on the server. A follow-up needs its own cortex_ask call with its own question, not a re-read of the previous result.

cortex_recall

Semantic-only vector search, returning source excerpts rather than a composed answer. Reach for it when you want nearest-neighbour matches on meaning and intend to do your own synthesis.

{ "query": "onboarding friction", "limit": 20 }

It takes query and limit, nothing else. There are no time bounds on cortex_recall; use cortex_ask when you need them.

Reading the response

cortex_ask returns evidence, not prose. There is no model on the server side composing an answer: you get a ranked result set with provenance, and your agent does the reasoning over it. An abridged response looks like this:

{
  "query": "What did we decide about pricing last quarter?",
  "results": [
    {
      "universal_id": "gmail_messages:18f2c9a...",
      "score": 0.412,
      "title": "Re: tier pricing",
      "preview": "Agreed: Solo goes to $19.99/seat from July.",
      "source_table": "gmail_messages",
      "source_url": "https://mail.google.com/mail/u/0/#inbox/18f2c9a...",
      "age_seconds": 3421,
      "signals": { "vector_rank": 1, "text_rank": 3 }
    }
  ],
  "freshness": [
    { "source_id": "google-workspace", "last_success_at": "2026-08-26T18:02:00Z", "status": "ok" }
  ],
  "signals": { "vector_candidates": 40, "text_candidates": 22, "graph_added": 6 },
  "took_ms": 380,
  "graph_url": "https://mitosislabs.ai/graph?office=4d38b034-...",
  "cited_graph_url": "https://mitosislabs.ai/graph?office=4d38b034-...&cite=gmail_messages%3A18f2c9a...",
  "memory": {
    "office_id": "4d38b034-...",
    "office_name": "Mitosis Labs",
    "graph_url": "https://mitosislabs.ai/graph?office=4d38b034-..."
  }
}

Four fields carry most of the operational meaning:

resultsobject[]

The evidence. Each item has a universal_id (this is the citation: pass it to cortex_remember as source_universal_ids), a fused score, a preview, the source_table it came from, and a source_url when the original is linkable. There is no top-level citations array: the ids live here.

freshnessobject[]

Per-source sync state. A confident-looking answer drawn from a source that last synced a week ago is a different claim from the same answer drawn from one that synced an hour ago, and only this field tells them apart.

cited_graph_urlstring

Deep link into the user's own graph with the cited nodes highlighted. Good to surface directly to a human who asks "where did that come from?" Absent when the answer cited nothing, because an empty &cite= highlights nothing. graph_url is the same graph without the citations pre-selected, and is always present.

memory.office_namestring

Which memory answered. Note that a connector's display name is text the user typed and identifies nothing. This field is the real one.

The same call from the SDK and the CLI

The MCP tools above are one surface over the retrieval engine. From TypeScript the same call is cortex.answer(), whose request field is query rather than question, and whose results carry per-item scores and signals instead of a composed prose answer.

const cortex = client.cortex(officeId);
 
const answer = await cortex.answer(officeId, {
  query: 'What did we decide about pricing last quarter?',
  limit: 10,
  since: '2026-04-01T00:00:00Z',
});
 
for (const r of answer.results) {
  console.log(r.score.toFixed(3), r.title, r.universal_id);
}
// Attributes writes, and carries provenance from ask() into remember().
const memory = client.cortex(officeId).forAgent(officeId, 'atlas');
 
const evidence = await memory.ask('What did we decide about pricing last quarter?');
 
evidence.top;             // best-scored result
evidence.isEmpty;         // nothing matched at all
evidence.isFresh(3600);   // did every source sync within the last hour?
evidence.universalIds;    // the ids remember() will link against
evidence.graphUrl();      // /graph deep link highlighting exactly these nodes
 
console.log(evidence.context());   // citation-formatted block for a prompt
mi cortex ask "What did we decide about pricing last quarter?" --office "$OFFICE"
 
mi cortex answer "..." --office "$OFFICE" --limit 10 \
  | jq -r '.results[] | "\(.score)\t\(.title)\t\(.universal_id)"'
MCP fieldSDK equivalent
results[].universal_idanswer.results[].universal_id, or evidence.universalIds
cited_graph_urlevidence.graphUrl(), or graph_url in mi cortex ask --json
per-source sync stateanswer.freshness[], or evidence.isFresh()

Full worked example, including ingest: ingest and query.

Handling a gap instead of guessing

The mistake to avoid is treating weak results as an answer. Check for the gap blocks described in how memory works before you compose a reply:

const res = await askMemory(question);
 
if (res.memory_state) {
  // Nothing connected yet. Offer res.memory_state.cta.url, don't apologise
  // for a search that never had anything to search.
} else if (res.source_gap) {
  // The graph has data, just not this kind. Name the missing source and
  // offer res.source_gap.cta.url.
} else if (res.possible_source_gap) {
  // Answer, but hedge: these may be near-misses.
} else {
  // Trustworthy: either a real answer, or a real absence.
}

The cta.url in each block opens the user's own dashboard page for connecting sources. Handing it over is safe: the link only shows them that page, and connecting is an authorization they perform there.