---
name: mubit-memory
description: >
  Give any AI agent persistent, cross-session memory using a Mubit instance:
  store facts and lessons, recall them with fast direct (non-LLM) retrieval,
  and close the learning loop with outcomes and reflection. Use this skill
  whenever an agent needs to remember things across sessions/runs, learn from
  successes and failures, or retrieve prior context with sub-second latency.
---

# Using Mubit as agent memory — at full power

Mubit is a hosted memory runtime for agents. You (the agent reading this) are
already an LLM — so you almost never need Mubit's server-side LLM to interpret
memory for you. **The single most important thing in this skill: use the
direct, non-LLM-routed retrieval paths by default.** They return the same
ranked evidence (including cross-session lessons) with zero LLM calls,
typically 30–200 ms instead of 4–9 s for the default agent-routed mode.
Reserve agent-routed queries for the rare case where you want Mubit's server
to synthesize an answer for another consumer.

## Setup

You need two values (ask the user if absent; never invent them):

```bash
export MUBIT_ENDPOINT="https://<your-instance-endpoint>"   # from the Mubit console
export MUBIT_API_KEY="mbt_..."                             # instance API key
```

Auth is `Authorization: Bearer $MUBIT_API_KEY` on every request (a bare key
without `Bearer` also works). All examples below work with either the SDK or
raw HTTP — pick whichever your environment supports.

- **Python**: `pip install mubit-sdk`, then `from mubit import Client`. The
  client auto-reads `MUBIT_ENDPOINT` / `MUBIT_API_KEY` from env:
  `client = Client()`.
- **JavaScript**: `npm install @mubit-ai/sdk`, `import { Client } from
  "@mubit-ai/sdk"` — same env vars. Prefer `new Client({ transport: "http" })`
  (see JS gotchas at the end).
- **Raw HTTP**: `POST $MUBIT_ENDPOINT/v2/...` with the Bearer header and JSON
  bodies, shown throughout.

Everything is scoped to a `run_id` (a session/workspace string you choose).
The SDK injects an ambient run id inside `with client.run("my-session"):`
(Python) / `client.withRun("my-session", async c => {...})` (JS); over raw
HTTP you pass `run_id` in every body. Runs are created implicitly on first
write — there is no create-run call. Run ids are namespaced per API key's
user, so two keys writing `"session-1"` get two different runs (a run belongs
to whoever wrote it first; others get a 403).

## Choosing a read path (memorize this table)

| Path | LLM calls | Embedding calls | Lesson overlay | Typical use |
|---|---|---|---|---|
| `lookup` (keyed) | 0 | **0** | no | exact reads by metadata match — fastest path in the system (~1–60 ms) |
| `core search` | 0 | 1 | no | raw semantic top-k, no extras |
| **`recall` with `mode="direct_bypass"`, `evidence_only=True`** | **0** | 1–3¹ | **yes** | **DEFAULT: ranked evidence + cross-session lessons + working memory, no LLM (~30–250 ms)** |
| `recall` (default `agent_routed`) | 2 (router + synthesis) | 2·V·R+1 (≈7–13)² | yes | only when you want Mubit's own LLM to write `final_answer` (~4–9 s) |
| `get_context` | 2 (one discarded!) | several | yes | pre-formatted prompt block; convenient but pays full LLM cost — avoid on hot paths |

¹ semantic lane + history lane + lesson overlay. `budget="low"` skips the
history lane; `prefer_current_run=True` skips the overlay; passing your own
`embedding` makes it 0.
² V = LLM query variants (≤3 default), R = consulted runs.

Rules of thumb:
- Reading structured records back, checking config/state, exact IDs → `lookup`.
- "What do I know about X?" → **direct-bypass evidence-only recall** (below).
- You want lessons applied to a new task → same direct-bypass recall; lessons
  arrive as evidence items with `retrieval_mode: "lesson_overlay"`.
- Never call agent-routed recall inside a loop.
- You are the LLM: if a recall misses, rephrase and retry — two direct recalls
  cost ~100 ms, still ~50× cheaper than one agent-routed call. You are doing
  the query-router's job yourself, for free.

## The default recall (direct, non-LLM, full power)

Python:

```python
from mubit import Client
client = Client()  # reads MUBIT_ENDPOINT / MUBIT_API_KEY

with client.run("sprint-42"):
    r = client.recall(
        query="How should I search for an invoice in the billing tool?",
        mode="direct_bypass",     # skip the query-router LLM
        evidence_only=True,       # skip the answer-synthesis LLM
        limit=8,
    )
    for ev in r.get("evidence", []):
        # ev: content, score (0-1 similarity), entry_type, run_id,
        #     retrieval_mode ("semantic" | "lesson_overlay" | "working_memory" | ...),
        #     knowledge_confidence (trust, distinct from score), is_stale
        print(ev["retrieval_mode"], round(ev["score"], 3), ev["content"][:80])
```

Raw HTTP (identical semantics):

```bash
curl -s "$MUBIT_ENDPOINT/v2/control/query" \
  -H "Authorization: Bearer $MUBIT_API_KEY" -H 'Content-Type: application/json' \
  -d '{"run_id":"sprint-42",
       "query":"How should I search for an invoice in the billing tool?",
       "mode":"direct_bypass","evidence_only":true,"limit":8}'
```

In this mode `final_answer` is `""` and `confidence` is `0.0` **by design**
(`routing_summary` ends in `:evidence_only`) — the ranked `evidence` array is
the product. You do the reasoning; that's the point.

Useful optional fields on the same call:
- `entry_types: ["fact","lesson"]` — filter by type. **Caution**: the server
  classifies stored types itself (your "decision" may be stored as `trace`),
  so an item surfaces only if its *stored* type is listed — omit this filter
  unless you've checked what actually got stored.
- `include_linked_runs: true` — also search runs linked via
  `client.advanced.link_run`.
- `prefer_current_run: true` — drop the cross-run lesson overlay (pure
  current-run evidence, one embedding call fewer).
- `rank_by: "freshness"` — recency-weighted ranking; `budget: "low"` — smaller
  candidate pool, skips the history lane, cheapest control-plane recall
  (**the cross-run lesson overlay still applies** with `budget="low"`).
- `min_timestamp`/`max_timestamp` (unix secs) — time-boxed recall.
- `limit` is clamped to 50; the query text is capped at 32 KiB.

### Exact reads: keyed lookup (zero embedding, deterministic)

When you know what you're looking for structurally, don't do semantic search
at all:

```python
rows = client.lookup(               # Python typed helper; returns a LIST
    session_id="sprint-42",
    match=[{"kind": "decision"}, {"ticket": {"$in": ["MB-12", "MB-19"]}}],
    limit=100,
)
```

```bash
curl -s "$MUBIT_ENDPOINT/v2/core/lookup" \
  -H "Authorization: Bearer $MUBIT_API_KEY" -H 'Content-Type: application/json' \
  -d '{"run_id":"sprint-42","match":[{"kind":"decision"}],"limit":100}'
```

`match` clauses are OR-combined; each clause matches stored metadata fields
exactly (`{"$in": [...]}` supported). Response is a bare JSON array of
`{id, run_id, metadata, created_at, updated_at}`.

Facts you must know for lookup to work:
- **Only `batch_insert` writes lookup-matchable metadata.** Metadata passed to
  `remember()` is NOT stored as match keys (it may surface in recall evidence,
  but keyed lookup cannot see it). Write records you'll need to look up later
  via `batch_insert` (next section) — its `metadata_json` keys become match
  keys and the item text is mirrored into `metadata.content`, so lookup alone
  reconstructs the full record.
- A run also contains **system rows** (ingestion events, step-outcome traces),
  so an empty `match` returns more rows than you wrote — always filter by your
  own keys.
- Each row's `run_id` comes back **namespaced** (`state::<uid>::<your-run-id>`)
  — never string-compare it with the run id you wrote; you'll need this form
  for `core search` scoping below.

### Raw semantic search (no overlay)

```bash
curl -s "$MUBIT_ENDPOINT/v2/core/search" \
  -H "Authorization: Bearer $MUBIT_API_KEY" -H 'Content-Type: application/json' \
  -d '{"query":"flaky retry logic in payment worker","k":10}'
```

Returns a bare array of `{id, score, metadata}`. Use when you want top-k
similarity and nothing else (no lessons, no working memory).

**Run-scoping trap**: core search does NOT translate your run id. Memory
written through the control plane lives under a namespaced run
(`state::<uid>::<your-run-id>`), so `{"run_id": "sprint-42"}` here silently
returns nothing. Either omit `run_id` (search the whole instance) or pass the
namespaced id copied from any lookup row's `run_id` field. If you don't want
to deal with this, use direct-bypass recall (above), which takes your plain
run id and is nearly as fast.

**Policy note**: `core search`, `lookup`, and `mode="direct_bypass"` are all
gated by the instance's direct-access policy dials (ON by default on hosted
instances). If an operator has disabled direct search you'll get a 403
("disabled by policy") from BOTH `core search` and `direct_bypass` — the
fallback that always works is `recall(evidence_only=True)` *without* the mode
flag: the router LLM runs (one LLM call, slower) but synthesis is skipped and
you still get the evidence array.

## Writing memory — two write paths

| Write path | Latency to accept | Visible after | LLM enrichment | Lookup-matchable metadata |
|---|---|---|---|---|
| `remember()` / ingest | ~30 ms (`wait=False`) | ~2–4 s/item, ordered queue | yes (background) | **no** |
| `batch_insert` | ~0.2–1.5 s (synchronous) | immediately | no | **yes** |

Use `remember()` for narrative memory (facts, observations, lessons) that you
will retrieve semantically. Use `batch_insert` for structured records (config,
decisions, tickets) that you will fetch back by key with `lookup`.

### Narrative memory: `remember()`

```python
with client.run("sprint-42"):
    client.remember(
        content="payments-worker retries are capped at 3; the 4th attempt dead-letters.",
        item_id="fact-payments-retry-cap",        # stable id => idempotent + retry-safe
        user_id="agent-shankha",                  # partition; must match on recall
        wait=False,                               # fire-and-forget on hot paths
    )
```

- `wait` defaults to `True`, which blocks ~2–4 s per item while the full
  ingestion pipeline (including background LLM enrichment) completes. On hot
  paths pass `wait=False` (~30 ms) and use the **write barrier** below before
  any dependent read.
- Always pass a stable `item_id`: it is the idempotency key **within a run**
  (same id in the same run dedupes a retry; the same id in another run is a
  separate memory).
- Re-remembering near-identical content may be *reconciled* (absorbed into or
  superseding the existing entry) rather than stored again — by design; verify
  writes by recalling content, not by counting rows.

**The write barrier** — ingestion is processed in order per run, so after a
burst of `wait=False` writes you only need to wait for the *last* job and
everything before it is ingested too:

```python
acc = client.remember(content="...", item_id="last-one", wait=False)
job_id = acc["job_id"]
# ...do other work...
while not client.advanced.get_ingest_job({"job_id": job_id, "run_id": run})["done"]:
    time.sleep(0.5)
# every write in this run up to "last-one" is now recall-visible
```

Raw HTTP: `POST /v2/control/ingest` with
`{"run_id": "...", "items": [{"item_id", "text", "user_id", ...}]}` returns
`{"job_id": ...}`; poll `GET /v2/control/ingest/jobs/<job_id>?run_id=...`.

### Structured records: `batch_insert`

```python
client.advanced.batch_insert({          # JS: client.advanced.batchInsert
    "run_id": "sprint-42",
    "deduplicate": False,
    "items": [{
        "item_id": "decision-MB-12",
        "text": "MB-12: adopt idempotency keys on all outcome writes.",
        "metadata_json": json.dumps({"kind": "decision", "ticket": "MB-12"}),
        "source": "agent",
    }],
})
```

Synchronous, no LLM, visible immediately, and — uniquely — its `metadata_json`
keys become `lookup` match keys with the text mirrored to `metadata.content`.
Keep batches to ~10–20 items (items are embedded inline; large batches hit the
30 s request timeout). On the wire: `POST /v2/control/batch_insert`, response
`{"count", "node_ids", "item_results": [{"item_id","node_id","success"}]}`.

### Lessons: the cross-session currency

A **lesson** is a memory Mubit injects into recalls in *other* runs (the
lesson overlay). To make knowledge cross sessions deliberately:

```python
client.remember(
    content="Always search billing by invoice ID; full-name search times out.",
    item_id="lesson-invoice-id",
    intent="lesson",
    lesson_type="success",        # success | failure | observation | rule | preference
    lesson_scope="global",        # THE cross-session switch: run < session < global
    lesson_importance="high",
    user_id="agent-shankha",
)
```

`lesson_scope="global"` makes a memory reachable from a brand-new, unlinked
run — and lesson-intent writes skip heavy enrichment, landing in ~150 ms.
An `"org"` scope exists for tenant-wide sharing (write it as
`lesson_scope="org"`; the SDK's `share=` alias refuses it) — most agents
should stick to `"global"`. `client.learned("...")` is a one-line shorthand
for a session-scoped success lesson.

## The learning loop (make the memory improve itself)

Report outcomes as you work — failures are the signal the server learns from:

```python
client.record_step_outcome(
    step_id="deploy-1", step_name="helm-upgrade",
    outcome="failure",                  # success | failure | partial | neutral
    signal=-0.6,                        # optional, [-1,1]; the label alone also counts
    rationale="values file pointed at the staging registry",
    directive_hint="check registry host in values before helm upgrade",
)
client.record_outcome(reference_id="lesson-invoice-id", outcome="success",
                      signal=0.8)      # reinforce a lesson that helped you
```

The server auto-reflects as activity accrues (ingest volume, failure streaks).
You can force it at a natural boundary — end of a session, after a hard bug:

```python
result = client.reflect()               # extracts lessons from this run's history
listed = client.lessons({"run_id": "sprint-42", "limit": 50})["lessons"]
```

Three things to know about extracted lessons:
- **`reflect()`-extracted lessons start with `scope="run"`** — they do NOT
  surface in other runs immediately. The server promotes recurring, validated
  lessons to wider scopes on its own over time. If you learned something *now*
  and need it in the next session *now*, also write it explicitly with
  `remember(intent="lesson", lesson_scope="global")` — that is the guaranteed
  cross-session write.
- **Reflect only sees ingested items**: after `wait=False` writes, run the
  write barrier first or reflection will find nothing and store zero lessons.
- `reflect()` is an LLM operation (seconds) — call it at session end, not in
  loops; `client.run(..., reflect_on_exit=True)` does it on clean exit.
  Naming drift: `reflect()` returns lessons keyed `lesson_id`; `lessons()`
  returns the same objects keyed `id`. `lessons()` takes a payload dict with
  wire names (`run_id`, not `session_id`); `{"run_id": ""}` lists across runs.

## Cross-session recall: the matching rules

A memory written in session A surfaces in session B only if the read matches
the write. Check these five whenever recall comes back empty:

1. **Scope**: same `run_id`, or a linked run + `include_linked_runs=True`, or
   the item is a lesson with `lesson_scope` ≥ `"session"` (use `"global"`).
2. **`user_id`**: read and write must use the same value. Leaving it unset on
   BOTH sides is a valid pair; setting it on one side only is a miss.
3. **`entry_types`**: if you filter, the *stored* type must be in your list —
   when in doubt, don't filter.
4. **`lane`**: lane-tagged writes need the same `lane` on recall.
5. **Identity**: same API key (or keys mapping to the same user) — run ids are
   namespaced per user.

The canonical cross-session pattern:

```python
with client.run("session-a"):
    client.remember(content="...", intent="lesson", lesson_type="success",
                    lesson_scope="global", user_id=AGENT_ID, item_id="lesson-x")

with client.run("session-b"):                       # brand-new, unlinked
    r = client.recall(query="...", mode="direct_bypass", evidence_only=True,
                      user_id=AGENT_ID, limit=8)
    # the lesson arrives with retrieval_mode == "lesson_overlay"
```

On a shared instance the overlay returns global lessons from **all** your
runs. To attribute a lesson to a specific origin, check its evidence `run_id`
— it is the composite `state::<user>::<origin_run_id>`, so match on the
suffix, never on equality with your own run id.

## When you actually want the server's LLM

`mode="agent_routed"` (the default if you pass nothing) runs a query-router
LLM, fans the query out as several variants, then a synthesis LLM writes
`final_answer` with `citations` (indices into `evidence`) and `confidence`.
Cost: two LLM round-trips (seconds) plus the multiplied embedding calls.
Legitimate uses: producing a memory-grounded answer for a human/another system
without another LLM in the loop, or `schema="..."` structured extraction
server-side. For your own reasoning, prefer the evidence and think yourself.

## Operational notes & gotchas

- **Latency budget** (live-measured): `lookup` ~1–60 ms ≪ `core search` /
  direct-bypass recall ~30–250 ms (one to three embedding hops) ≪ agent-routed
  / `get_context` / `reflect` ~4–9 s (LLM-bound). Everything runs under a 30 s
  server timeout.
- **Cold starts**: the first embedding call on an idle instance can exceed the
  SDK's 30 s client timeout even for tiny requests. Use stable `item_id`s and
  simply retry on a transport timeout (idempotent, no duplicates), or pass
  `timeout_ms=60000` to `Client()`.
- **`evidence[].metadata_json` is a string** — `json.loads` / `JSON.parse` it.
  On writes, the SDK turns your `metadata` dict into that string for you; over
  raw HTTP you encode it yourself.
- **Response shapes differ**: `recall` → dict with `evidence`; `lessons()` →
  dict with `lessons`; `lookup` and `core search` → bare arrays.
- **Empty-result fallback**: an evidence list where every item has
  `retrieval_mode: "recency_fallback"` and `score: 0.5` means "nothing
  matched; here's what's recent" — treat as a miss, not a hit.
- **Scores**: `score` is retrieval similarity; `knowledge_confidence` is the
  server's trust in the item (reinforced by outcomes). `is_stale: true` items
  have a superseding entry — check `superseded_by`.
- **Python retries** are automatic only for reads and writes carrying an
  `idempotency_key` (a stable `item_id` gives you this). **JS retries every
  op** up to 3× — always pass `idempotency_key` on JS writes that lack an
  `item_id`.
- **JS only**: use `transport: "http"` — auto mode can return camelCase (gRPC)
  or snake_case (HTTP) field names depending on connectivity, and `lookup`
  needs HTTP anyway. There is no typed `lookup` helper in JS: call
  `client.core.lookup({run_id, match, limit}, {transport: "http"})`.
- **Deprecation warnings**: `client.query(...)` / `client.ingest(...)` warn;
  the supported spellings are the helpers used above plus
  `client.advanced.<op>(payload)` for anything exotic (snake_case ops in
  Python, camelCase in JS).
- **Don't put secrets in memory content.** Memory is durable and surfaces in
  later sessions by design.

## Quick self-test (run once after setup)

```python
from mubit import Client; import json, uuid
client = Client(); tag = uuid.uuid4().hex[:6]
with client.run(f"skill-check-{tag}"):
    client.remember(content=f"self-test marker {tag}", item_id=f"m-{tag}", wait=True)
    client.advanced.batch_insert({"run_id": f"skill-check-{tag}", "items": [
        {"item_id": f"r-{tag}", "text": f"self-test record {tag}",
         "metadata_json": json.dumps({"kind": "selftest"}), "source": "selftest"}]})
    hits = client.recall(query=f"self-test marker {tag}",
                         mode="direct_bypass", evidence_only=True, limit=5)
    assert any(tag in e["content"] for e in hits["evidence"]), "recall miss"
    assert client.lookup(match=[{"kind": "selftest"}]), "lookup miss"
print("mubit-memory: OK")
```
