Documentation
Hivemind gives your agents persistent shared memory and a working-context compiler: store what matters, recall it semantically, and assemble a token-budgeted, fully audited prompt for every turn. Your model, your key — Hivemind never calls an LLM.
Quickstart
1. Get your key
- Sign in and subscribe (14-day trial, card up front, nothing billed today).
- Under Storage, connect your Qdrant cluster — its URL and API key. Your memory lives there, not with us. Any reachable Qdrant works: self-host the open-source server on your own infrastructure, or use Qdrant Cloud — its free tier is enough to start.
- Under API Keys, create a key. It is shown once — store it as
HIVEMIND_API_KEY.
2. Integrate — any language
Hivemind is a plain HTTPS API: the whole loop is two JSON calls per exchange, from any language. The wire protocol below is the complete integration contract, with the exchange translated into curl, JavaScript, Python, and Go.
3. Prove the connection
A new workspace starts empty — nothing is written into your cluster except what you and your agents store. Prove the connection with one store and one recall:
curl -s https://api.hivemind.militant.ai/hivemind/v1/create_memory \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Hivemind connected: this workspace remembers from here on.", "metadata": {"topic": "setup"}}'
curl -s https://api.hivemind.militant.ai/hivemind/v1/recall_memory \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "when did this workspace start remembering?", "top_k": 5}'
Point your agents at this documentation for how the system works — the loop, what to store, and the full reference are written to be read by them as much as by you.
The loop: three lines inside your agent
packet = POST /turn { user_input, session, budget, pair-id, history } # store → recall → compile
reply = your model, run on packet.bundle.messages # your model, your key
POST /conversation/store { reply, same pair-id } # completes the exchange
Working code for that loop, in four languages, is in the wire protocol.
One /turn is one request: the service stores the user
message (receipted), semantically recalls relevant memories and past
conversation, and compiles local history + recalled records + active holds
into a token-budgeted bundle. The response’s bundle.messages is a
contiguous, internally-ordered block: use it as the entire
prompt, or surround it — your system prompt above, turn-local instructions
below; the packet keeps its place either way. Never insert into or reorder
inside the block, and if you wrap it, size
budget_total as your model window minus your wrapper.
The store call files the reply and completes the exchange: the whole
loop is two calls, with your model between them.
Conversation is remembered as call/response exchanges.
Recall matches your query against both sides of every past exchange and
returns whole exchanges — one result slot per exchange, rendered
call-then-response, never an answer without the message that produced it.
Facts that appear only in a reply are just as findable as the calls that
prompted them. The recall pool sizes itself from your session's token
budget, and the compiler's budget admission decides what enters the bundle;
pass recall_top_k only to force a fixed pool. The store call completes the exchange — treat it as part
of the loop. Durable facts that should stand alone — decisions, outcomes,
lessons — belong in deliberate memory.
The wire protocol — any language
The service is language-agnostic. One exchange is two HTTPS calls, with your model between them; any client library is a convenience layer over exactly this.
Call 1 (/turn) stores the user message, recalls, and
returns the compiled prompt packet — the response's
data.bundle.messages is the prompt block for your model,
data.receipt the audit record. Call 2
(/conversation/store) records the reply, completing the
exchange with the same exchange_pair_id and
turn_number. The whole exchange, translated:
# 1. compile the turn
curl -s https://api.hivemind.militant.ai/hivemind/v1/turn \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_input": "Can we ship the migration tonight?",
"session_id": "session-1",
"turn_number": 7,
"budget_total": 8192,
"exchange_pair_id": "xp-7f3a1c",
"messages": [{"role": "user", "content": "Can we ship the migration tonight?"}]
}'
# ... run your model on data.bundle.messages ...
# 2. record the reply (same exchange_pair_id + turn_number)
curl -s https://api.hivemind.militant.ai/hivemind/v1/conversation/store \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Yes — window is clear until 03:00 UTC.",
"session_id": "session-1",
"turn_number": 7,
"role": "assistant",
"exchange_pair_id": "xp-7f3a1c"
}'
const BASE = "https://api.hivemind.militant.ai/hivemind/v1";
const HEADERS = {
"Authorization": `Bearer ${process.env.HIVEMIND_API_KEY}`,
"Content-Type": "application/json",
};
const pairId = crypto.randomUUID(); // fresh per turn
// 1. compile the turn
const turn = await fetch(`${BASE}/turn`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({
user_input: userInput,
session_id: "session-1",
turn_number: 7,
budget_total: 8192,
exchange_pair_id: pairId,
messages: [{ role: "user", content: userInput }],
}),
}).then(r => r.json());
const reply = await callYourLlm(turn.data.bundle.messages);
// 2. record the reply (same exchange_pair_id + turn_number)
await fetch(`${BASE}/conversation/store`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({
content: reply,
session_id: "session-1",
turn_number: 7,
role: "assistant",
exchange_pair_id: pairId,
}),
});
import os, uuid, requests # any HTTP client works the same way
BASE = "https://api.hivemind.militant.ai/hivemind/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['HIVEMIND_API_KEY']}"}
pair_id = str(uuid.uuid4()) # fresh per turn
# 1. compile the turn
turn = requests.post(f"{BASE}/turn", headers=HEADERS, json={
"user_input": user_input,
"session_id": "session-1",
"turn_number": 7,
"budget_total": 8192,
"exchange_pair_id": pair_id,
"messages": [{"role": "user", "content": user_input}],
}).json()
reply = call_your_llm(turn["data"]["bundle"]["messages"])
# 2. record the reply (same exchange_pair_id + turn_number)
requests.post(f"{BASE}/conversation/store", headers=HEADERS, json={
"content": reply,
"session_id": "session-1",
"turn_number": 7,
"role": "assistant",
"exchange_pair_id": pair_id,
})
base := "https://api.hivemind.militant.ai/hivemind/v1"
pairID := uuid.NewString() // fresh per turn
post := func(path string, payload map[string]any) map[string]any {
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", base+path, bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("HIVEMIND_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
var out map[string]any
json.NewDecoder(res.Body).Decode(&out)
return out
}
// 1. compile the turn
turn := post("/turn", map[string]any{
"user_input": userInput, "session_id": "session-1",
"turn_number": 7, "budget_total": 8192,
"exchange_pair_id": pairID,
"messages": []map[string]any{{"role": "user", "content": userInput}},
})
reply := callYourLLM(turn["data"].(map[string]any)["bundle"])
// 2. record the reply (same exchange_pair_id + turn_number)
post("/conversation/store", map[string]any{
"content": reply, "session_id": "session-1",
"turn_number": 7, "role": "assistant",
"exchange_pair_id": pairID,
})
The protocol's rules — every client must honour these:
- Pairing. Generate a fresh
exchange_pair_idper turn; the record call must reuse it and the turn number, so the exchange is stored as one call/response pair. Any collision-resistant unique string works — the project's convention is a time-ordered UUIDv7, so the id doubles as a temporal datum. - Aborted generation: record nothing. If the user cancels mid-stream, make no call — the stored user message stands as an unanswered call, and the next message is a new turn. Never record a partial reply as if it was said, and never send empty content.
- Never retry a timed-out turn. A timeout does not mean the request failed — re-sending can store the message twice. Retry only requests that failed before any response began.
- Honour
Retry-After. A429carries it; back off that many seconds and retry. That is the service asking your client to slow down, not an error. - Budget ceiling.
budget_totalat most 128,000; a400names the limit. - Optional: select a project with the
X-Hivemind-Projectheader.
Every other endpoint speaks the same JSON envelope with the same auth header. The full API reference documents every endpoint — request fields, response shapes, errors — with working examples in all four languages, plus the complete client implementation guide.
The working context — representative output
This is what one /turn (or /context/compile) hands back,
rendered: the trace, the admitted items, the accounting, and the compiled
messages your model receives. Toggle the response JSON to see the exact
wire shape.
TRACE
- Session ID
- session_001
- Turn Number
- 14
- Built At
- 2026-07-05T14:32:11+00:00
- Event ID
- event_001
- Trace ID
- trace_001
- User Input
- Find the lowest-risk path to stabilise checkout latency before peak traffic.
- Context Messages
- 13
ITEMS (KIND · SOURCE, TOP 5 OF 13)
-
MEMORY · HIVEMINDmemory-9d41c2
Checkout latency spiked during the payment provider failover; regional failback plus cache warmup restored p95 within 11 minutes.
-
DIRECTIVE · DIRECTIVEcontext-briefing-14
Prefer reversible mitigations and preserve checkout stability during active traffic.
-
MESSAGE · SYSTEMcontext-artifacts-14
Artifacts: Checkout Latency Playbook
v3.2; SLO dashboard shows p95 above threshold inus-east-1. -
MESSAGE · RECALLED_CONVERSATIONrecalled-conv-310
Last incident we deferred config changes and scaled the payment worker pool first.
-
HOLD · SYSTEMhold-8c2f
Checkout change freeze: avoid changes that increase regression risk during active checkout traffic.
+ ranked, deduped, and trimmed before final assembly
ACCOUNTING (API DATA)
-
POLICYbalanced
Balanced admission inside
max_context_tokenswith per-class caps on memory, tool, and hold items. -
COUNTSsources
Admitted items counted by source: hivemind, conversation, recalled_conversation, directive, system.
-
DECISIONSdiagnostic
Every candidate gets an action: included, dropped_budget, dropped_policy, or trimmed_budget. Not prompt content.
-
TRACEsnapshot
Compiler diagnostics remain inspectable alongside the assembled bundle.
MESSAGES (COMPILED 13)
-
SYSTEMstatus line
Context: 3220/4096 tokens | turn 14 | 13 items
-
SYSTEMdirective
Prefer reversible mitigations and preserve checkout stability during active traffic.
-
USERactive input
Find the lowest-risk path to stabilise checkout latency before peak traffic.
+ the remaining 10 messages of this example's bundle: memory, artifacts, hold, and recalled conversation
{
"ok": true,
"data": {
"bundle": {
"messages": [
{ "role": "system", "content": "Context: 3220/4096 tokens | turn 14 | 13 items", "tool_call_id": null, "name": null },
{ "role": "system", "content": "=== AGENT FIXED CONTEXT BUFFER ===", "tool_call_id": null, "name": null },
{ "role": "system", "content": "Prefer reversible mitigations and preserve checkout stability during active traffic.", "tool_call_id": null, "name": null },
{ "role": "system", "content": "Artifacts: Checkout Latency Playbook v3.2; SLO dashboard shows checkout p95 above threshold in us-east-1.", "tool_call_id": null, "name": null },
{ "role": "system", "content": "Hold - Checkout change freeze: avoid changes that increase regression risk during active checkout traffic.", "tool_call_id": null, "name": null },
{ "role": "system", "content": "Checkout latency spiked during the 2026-06-21 payment provider failover; regional failback plus cache warmup restored p95 within 11 minutes.", "tool_call_id": null, "name": null },
{ "role": "system", "content": "Scaling the payment worker pool from 8 to 14 replicas resolved queue backpressure without config changes.", "tool_call_id": null, "name": null },
{ "role": "system", "content": "CDN config rollout on 2026-06-28 correlated with elevated checkout errors; rollback was clean and reversible.", "tool_call_id": null, "name": null },
{ "role": "system", "content": "Checkout Latency Playbook summary: prefer traffic shaping, replica scaling, and cache warmup before any config change.", "tool_call_id": null, "name": null },
{ "role": "assistant", "content": "Last incident we deferred config changes and scaled the payment worker pool first.", "tool_call_id": null, "name": null },
{ "role": "user", "content": "That worked - latency recovered inside the traffic window.", "tool_call_id": null, "name": null },
{ "role": "assistant", "content": "Monitoring is live. Ready for the next directive.", "tool_call_id": null, "name": null },
{ "role": "user", "content": "Find the lowest-risk path to stabilise checkout latency before peak traffic.", "tool_call_id": null, "name": null }
],
"policy": {
"max_context_tokens": 4096,
"max_items": 50,
"max_memory_items": 4,
"max_tool_items": 10,
"max_hold_items": null,
"include_memory": true,
"include_tools": true,
"include_system": true,
"include_directives": true,
"include_holds": true,
"admission_strategy": "balanced"
},
"token_count": 3220,
"source_counts": { "hivemind": 4, "system": 4, "conversation": 2, "recalled_conversation": 2, "directive": 1 },
"budget_total": 4096,
"budget_available": 876,
"conversation_tokens": 310,
"hold_tokens": 84,
"trace": {
"session_id": "session_001",
"turn_number": 14,
"built_at": "2026-07-05T14:32:11+00:00",
"event_id": "event_001",
"trace_id": "trace_001",
"user_input": "Find the lowest-risk path to stabilise checkout latency before peak traffic.",
"budget_total": 4096,
"budget_used": 3220,
"budget_available": 876,
"conversation_tokens": 310,
"hold_tokens": 84,
"context_message_count": 13,
"source_counts": { "hivemind": 4, "system": 4, "conversation": 2, "recalled_conversation": 2, "directive": 1 },
"recalled_conversation_count": 2,
"recalled_memory_count": 4,
"recalled_memory_input_count": 7,
"semantic_memory_duplicates_filtered": 2,
"receipt_id": "e9b3c1a0-59f2-11f1-8d2e-0242ac120002",
"parent_receipt_id": null
}
},
"receipt": {
"receipt_id": "e9b3c1a0-59f2-11f1-8d2e-0242ac120002",
"operation": "context.compile",
"status": "ok",
"created_at": "2026-07-05T14:32:11+00:00",
"tenant_id": "tenant_acme",
"principal_id": "svc-runtime",
"agent_id": "agent_ops_01",
"session_id": "session_001",
"turn_id": "14",
"source_event_id": "event_001",
"trace_id": "trace_001",
"parent_receipt_id": null,
"request_id": "f21d7b6e-59f2-11f1-8d2e-0242ac120002",
"idempotency_key": null,
"inputs_ref": { "session_id": "session_001", "turn_number": 14, "budget_total": 4096, "recalled_conversation_count": 3, "recalled_memory_count": 7 },
"outputs_ref": { "token_count": 3220, "item_ids": ["context-briefing-14", "context-status-14", "context-artifacts-14", "hold-8c2f", "memory-9d41c2"], "source_counts": { "hivemind": 4, "system": 4, "conversation": 2, "recalled_conversation": 2, "directive": 1 } },
"metadata": { "trace_id": "trace_001", "event_id": "event_001" }
}
},
"request_id": "f21d7b6e-59f2-11f1-8d2e-0242ac120002"
}
Deliberate memory
# store a durable fact with your metadata
curl -s .../hivemind/v1/create_memory -H "$AUTH" -H "$JSON" -d '{
"content": "The staging database is rebuilt every Monday 03:00 UTC; do not schedule migrations near that window.",
"metadata": {"project": "atlas", "topic": "ops"}
}'
# recall it, semantically or filtered
curl -s .../hivemind/v1/recall_memory -H "$AUTH" -H "$JSON" \
-d '{"query": "when is it safe to run a staging migration?", "top_k": 5}'
curl -s .../hivemind/v1/recall_with_metadata -H "$AUTH" -H "$JSON" \
-d '{"query": "database", "metadata": {"project": "atlas"}, "top_k": 10}'
Store decisions and their rationale, lessons learned, durable facts,
and outcomes. Attach consistent metadata keys — they are what make
filtered recall and surgical, audited deletion possible later. Empty
recall on a new workspace returns [] — that is “nothing
relevant stored yet”, not an error. /recall_memory searches
deliberate memories only: conversation history is a
separate record class, recalled automatically inside the loop (or
explicitly via /conversation/recall).
The audit trail
curl -s ".../hivemind/v1/receipts?session_id=session-1&limit=50" -H "$AUTH"
Every operation — store, recall, compile, delete — emits a persisted receipt with lineage: what ran, what it consumed, and what it produced.
The context window
The compiled packet is not an opaque blob — it has a fixed anatomy, and knowing it is how you place every kind of content deliberately. A real compile looks like this (roles on the left):
system === AGENT FIXED CONTEXT BUFFER ===
system [your operator_briefing — persona, rules, mode flags]
system Context: 3210/8192 tokens | 2 holds active (350 tokens) | ...
system === HOLD === id: stop-order · title · tokens · held_since ...
system [artifacts_context — "Available files:" when supplied]
system === SEMANTIC RECALL BUFFER ===
system [recalled deliberate memories, relevance-ranked]
system === SEARCH RECALL BUFFER ===
user [recalled exchange: the call]
assistant [recalled exchange: the response]
system === ACTIVE CONVERSATION BUFFER ===
user [recent conversation, reading order]
assistant ...
user [the current message — always last]
Three things to read off that structure:
- The fixed context buffer is the boundary your integration works within.
Everything a developer would normally wrap around a prompt has a
named slot inside the packet instead — and content inside is
budget-managed, admission-decided, counted in the status line, and
receipted; content wrapped outside is invisible to all accounting.
External content Its slot System prompt / persona / rules operator_briefing— protected system directive, re-sent per turn, never storedUser profiling, standing operational state holds — persistent until released, labeled, token-visible Documents, file excerpts, uploads artifacts_context— this compile onlyMode flags, turn-local steering operator_briefingper-turn overrideTool definitions not context — your API's separate tools field - The model sees its own budget. The status line (third message) reports tokens used against budget, active holds and their cost, every single turn — the feedback that lets an agent manage its own window.
- The packet is self-labeling and position-independent. Its section markers announce its regions; nothing inside references absolute position. Stack your own messages above or below and every internal relationship holds — Hivemind can be the entire window or a region within one.
API surface
Every call. All responses arrive as
{"ok": true, "data": ..., "request_id": ...} envelopes; the
envelope carries them.
| Endpoint | What it does |
|---|---|
POST /turn | The composed exchange: store the call, inline recall (both searches), compile. Params: budget_total, messages (local history), operator_briefing, artifacts_context, holds, author, recall_top_k (omit for budget-dynamic), policy. Returns bundle + receipt + recall counts + warnings + timings |
POST /conversation/store | The reply half of the exchange, paired by exchange_pair_id |
POST /create_memory | Deliberate durable memory with your metadata |
POST /recall_memory | Semantic recall over deliberate memories only |
POST /recall_with_metadata | Semantic recall scoped by exact metadata matches |
POST /conversation/recall | Manual episode recall: session_id=None searches all sessions (one result per exchange); a session id replays that session in full (both halves, newest-first) |
POST /context/compile | Raw compile for custom loops built around your own recall |
GET /receipts | Audit trail, newest-first; filter by session_id / operation |
GET /usage | Storage, point counts, and the limits your subscription carries |
GET /export | Every record you own, paged; client.export_to_file(path) writes JSONL. Portability by contract |
POST /delete_by_metadata | Soft-deletes everything matching all given keys. Your key is the authorization (included with your subscription). Out of recall immediately, recoverable until purged, always receipted. Mind filter breadth — what your agents may delete is your harness's policy |
The turn, in detail
Everything turn() accepts, and what each input does:
| Parameter | Behavior |
|---|---|
user_input | The initiating message. Stored as the call half of a new exchange, used as the query for both recall searches, and always the packet's last message. Must be non-empty |
budget_total | The compile's token budget — the single number that governs the whole window. Set per session or per turn; if you wrap the packet, size it as your model window minus your wrapper. Maximum 128,000 — a larger value is rejected with 400 VALIDATION_ERROR whose details.max_budget_total names the limit. The budget bounds what is surfaced per turn, not what is remembered: memory is unbounded |
messages | Your local history: {role, content, timestamp} dicts, oldest first, real timestamps (the compiler orders conversation by them). Your client maintains and sends this buffer every turn, and rebuilds it from its own store after restarts (see Building a chat host) |
operator_briefing | Your system layer — compiled as a protected directive at the top of the packet, re-sent every turn, never stored, never recalled |
artifacts_context | One-compile content (document excerpts, file listings). Rendered in the fixed buffer this turn only; never stored |
holds | Pinned state riding every compile until you clear it — see the chat-host section for the lifetime contract |
author | Who produced this call — identity metadata, stored on the record and surfaced as the message's name |
recall_top_k | Omit for a budget-scaled candidate pool (the default and usually right); set to force a fixed pool, gated by your subscription's ceiling |
policy | Advanced: a compile-policy override (item caps, include switches) for this turn — most integrations never need it |
And everything it returns:
| Field | Contents |
|---|---|
bundle.messages | The compiled packet — the block you send to your model |
bundle.token_count / budget_total | The packet's measured size against the budget — the context meter |
receipt | The compile's audit record; receipt_id links it in the receipts trail |
stored | The stored call half's id — empty if the store degraded (see warnings) |
recalled_memories / recalled_conversation | How many records each recall search contributed to the compile |
warnings | Degradations that didn't fail the turn — a failed store or recall search reports here and the compile proceeds without it |
timings | Server-side phase breakdown, seconds: embed_s (encoding your input once, shared by both recall searches), store_and_recall_s (the store and both recall searches, run concurrently), completion_s (fetching missing exchange halves), compile_s (assembly and admission), total_s. A slow turn names its own bottleneck |
The client-side state these calls assume (history buffer, open exchange, holds) is specified in Building a chat host.
Conventional environment names
Nothing requires these, but examples and tooling use them:
| Env var | Meaning |
|---|---|
HIVEMIND_BASE_URL | Service root — the host only (https://api.hivemind.militant.ai); clients append /hivemind/v1 |
HIVEMIND_API_KEY | Sent as Authorization: Bearer <key>; identifies and tenant-binds in one step |
HIVEMIND_PROJECT | Optional project binding, sent as X-Hivemind-Project |
HIVEMIND_TIMEOUT | Client request timeout in seconds — compiles at large budgets are multi-second operations; 120 is a reasonable default |
HIVEMIND_BUDGET_TOTAL | Default compile budget when your host does not set one per session |
When storing exchange halves via
POST /conversation/store, put the initiating message's text
under additional_metadata.semantic_content on the
reply half so a recalled reply carries its question, and
additional_metadata.author on either half to attribute it.
Memory & metadata
Every stored record carries a record class, stamped at write time, and each recall path searches exactly one class — the classes never overlap:
| Class | What | Recalled by |
|---|---|---|
semantic | Deliberate memories (/create_memory) | /recall_memory, /recall_with_metadata, and the loop's semantic recall — relevance-ranked |
temporal | Conversation exchanges | The loop's conversation recall and /conversation/recall — returned newest-first |
diagnostic | Receipts, document references | Never semantically recalled; scrolled by explicit query only |
Metadata is the control surface. Two kinds of keys live on every record. Service-stamped keys are set for you at write time — filter on them freely, but don't overwrite them:
| Key | Meaning | Why you care |
|---|---|---|
type | What kind of record this is (conversation_exchange, usage_guide, your own values via metadata) | The broadest filter; conversation records are identified by it |
session_id / turn_number | Which conversation, which turn | Session replay, receipts filtering, chronological ordering of recalled exchanges |
role | Positional: which side of the exchange — user initiated (the call), assistant answered — not who was talking | Drives exchange pairing and rendering; identity lives in author, never here |
exchange_pair_id | The identity of one exchange — both halves share it, nothing else ever does | Drives one-slot-per-exchange dedupe and pair completion in recall |
recall_mode | The record class: semantic, temporal, or diagnostic | Decides which recall path can ever return this record |
storage_mode | inline or chunked (content over ~1,000 chars is split) | Chunked records reassemble transparently on recall; chunk_group_id ties the pieces |
timestamp | Unix time at storage | Orders conversation into reading order; drives newest-first returns |
recall_count | How many times recall has returned this record — incremented automatically after every recall | This is attention: it makes frequently-useful records gravitational sources in ranking (see below) |
semantic_relativity, resonance_score | Query-time relevance: the engine's similarity for this query, and the final ranking score after resonance | Read them off recall results to understand why something ranked where it did |
deleted_at | Soft-deletion stamp | Present = out of all recall, recoverable until purged |
Caller-set keys are yours, supplied via
metadata on remember() or
additional_metadata on stores. Every one becomes an
exact-match filter in recall_filtered and a deletion selector
in delete_by_metadata — consistent keys are what make
surgical operations possible later. The ones with built-in behavior:
| Key | Built-in behavior |
|---|---|
author | Who produced the message — surfaces as the chat message's name field in compiles and recalls; pass author on /turn and additional_metadata.author on the store |
semantic_content | On a reply half: the call text that produced it, so a reply recalled alone renders with its question — set it on the reply half of every store |
tags | A list; shared tags create a ranking linkage between records (see below), and tags filter like any key |
document_id | Ties records to a document; same-document records link in ranking, and it scopes recall_filtered for per-document recall |
| anything else | No built-in behavior — pure filter/delete selector. project, customer_id, agent_run: whatever your operations need to slice by |
Storage mechanics worth knowing: content over ~1,000 characters is
chunked (each chunk embedded from its own text, so long
content stays reachable at any depth; recall reassembles the whole record).
Deletion is soft — a deleted_at stamp removes
the record from all recall while remaining recoverable until purged. Ids
are time-ordered UUIDv7 — the identifier is also the temporal datum.
Recall & ranking
Two recall paths, never conflated: inline
— automatic inside turn(), on the raw user input, no query
authored by anyone — and manual — the explicit endpoints
with deliberate queries and explicit top_k. The inline path
runs two searches concurrently: one over deliberate memories (the
semantic record class) and one over conversation exchanges (the
temporal record class).
The exchange is the recall unit
Ranked results are deduplicated by exchange_pair_id: when
both halves of one exchange match your query, the better-scoring half
takes one result slot and the freed slot goes to the next
different record — so top-5 recall means five different past
exchanges, never the same one twice. Dedupe is by identity, not
similarity: three near-identical exchanges from three sessions have three
pair ids and all three can rank. Every recalled exchange then renders
whole, by one of two routes: a reply found alone carries its call via
semantic_content, and a call found alone gets its reply
fetched in one batched lookup before the compile. Session-scoped replay
(/conversation/recall with a session_id) is
exempt from all of this — replay returns every row verbatim.
Pool sizing
The pool is how many candidates recall hands the compiler; the
compiler's budget admission — not the pool — decides what enters the
packet. Omit recall_top_k and the pool scales with the
budget: budget_total / 256, floor 5, ceiling 50, then
clamped to your subscription's max_top_k. So a 2k-budget turn
considers 8 candidates per search, an 8k turn 32, a 13k+ turn 50. Pass an
explicit recall_top_k to force a fixed pool — explicit
values above your subscription's ceiling are rejected with a 400 rather than
clamped.
Ranking: relevance first, resonance second
The base score of every candidate is the engine's own similarity between your query and that record — nothing outranks being about what was asked. On top of that base, resonance adjusts the ordering:
- Attention. Every time recall returns a record, its
recall_countincrements (asynchronously — it never slows your request). High-attention records — the top quartile within the current candidate set — become sources. - Sources pull their relatives, not themselves. A
source never boosts its own score. It boosts candidates it is
structurally linked to, with strength by relationship:
Relationship Strength Why it pulls Same exchange 1.0 The other half of a proven-useful exchange is almost certainly relevant with it Same chunk group 0.8 Pieces of one long record belong together Same session, within ±2 turns 0.6 Conversational neighbourhood — what surrounded a useful moment Same document_id0.5 Material from one document travels together Shared tags0.3 Your own declared grouping, weakest because broadest - Distance-gated and capped. A source's influence is divided by its own distance from the query — a popular record that has nothing to do with the current question exerts effectively nothing — and the total boost is capped at 2×, so attention can bend the ordering but can never invert a clear relevance gap. Popularity cannot beat being right.
Two practical consequences: tagging and document ids aren't just filters — they create the linkage structure ranking acts through; and the system adapts to your workload over time, because what your agents actually use accrues attention. Cross-session conversation results return newest-first; the compiler then lays recalled exchanges into the packet in reading order.
Budget & admission
budget_total is the authority: the compiler admits content
until the budget is spent.
What fills the window, in what order
Protected core admits first, unconditionally: your
operator_briefing and the current user message are always in
the packet, whatever the budget. Everything else competes through
balanced admission across sources — each candidate item
belongs to one source, and sources are weighted:
| Source | What fills it | Priority |
|---|---|---|
conversation | The live history you sent in messages, newest prioritized | Highest — the active conversation has the highest priority in the window |
semantic | Recalled deliberate memories | Second — durable knowledge relevant to this turn |
search | Recalled past exchanges, in reading order | Third — episodic context from other sessions and older turns |
tool | Tool-output items, when a caller passes them | Fourth — current-turn scaffolding |
fixed | Holds, artifacts_context, section markers, the status line | Admitted with the fixed buffer; holds are capped only by max_hold_items if you set one |
Weights bias, they don't ration: an empty source simply doesn't compete. With no recall to place, conversation takes all the space; a short conversation leaves the window to recall. Nothing is reserved for a source that has no candidates.
Caps scale with the budget
Two item-count backstops exist so a degenerate compile can't admit
thousands of one-token fragments — and both scale so a large window always receives
enough candidates: total items = max(50, budget_total/128), memory
items = max(10, budget_total/512). At a 4k budget: 50 items,
10 memories. At 128k: 1,024 items, 256 memories.
The meter
The status line reports the economy inside the packet every
turn — Context: 3210/8192 tokens | 2 holds active (350 tokens) |
... — where the model can read it and act on it.
result.token_count / budget_total report the
same numbers to your code, and they are the authoritative source for any
context meter: local history length undercounts (it knows nothing about
recall), and your LLM provider's usage measures a different
tokenizer after your own additions.
Building a chat host
The two-call loop is the whole integration contract — but a long-lived, restartable, streaming host needs a layer around it that the loop alone doesn't provide. This is that layer, stated at the wire level: your client keeps a little state, and these are its rules.
- Your client's state is three things: a turn counter
(increment per turn), the open exchange (its
exchange_pair_idand turn number, held between the/turncall and its/conversation/store), and a rolling local history buffer — the last few dozen messages of this session (40 is a good default), each{role, content, timestamp}with a real unix timestamp. Send that buffer plus the current message asmessageson every/turn: it is the "recent conversation" the compiler combines with recalled memory, and the compiler orders it by timestamp — fabricated times scramble reading order. - Stream first, compile second.
/turndoes real work — a store, semantic recall, and a full context compile — and at large budgets that takes seconds, not milliseconds. If you call it before sending anything, streaming clients receive no bytes and many treat the connection as failed. Establish the stream first (headers or a keep-alive frame), then compile, then stream your model.data.timingscarries the server-side phase breakdown for every turn, so a slow one names its own bottleneck in your logs. - Restarts: the history buffer is held in your process
and is lost when that process stops. After a restart, rebuild it from your own
store — nothing is re-sent to the service; the buffer is purely what you
pass as
messages. Map your conversation id tosession_idso a restarted host lands in the same session; when a user clears a thread, start a new session id so the fresh thread doesn't inherit what they asked to be rid of. - Aborted generation: make no call. When a user
cancels or a stream drops, discard the open exchange and move on. The
user's message was already stored by
/turnand stays recallable as an unanswered call — which is what happened. Never store empty content (the service rejects it), never store a partial reply as cleanup — the one legitimate partial case is a host that deliberately keeps what was shown on screen as what was said, decided after the stream ends. - Recording the reply carries two metadata conventions.
On the
/conversation/storecall, put the initiating message's text underadditional_metadata.semantic_content— it lets a recalled reply carry its question when rendered — andadditional_metadata.authorto attribute it. Both optional, both worth doing. - Continuation hops:
rolemarks position in the exchange — which side initiated, which side answered — not human vs. machine. So feeding the model's own output back in as the next/turn(tool loops, inner monologue, "keep going") is correct use: the prior output is the initiating utterance of the next exchange, and recall will later find the exchange by it. - Authorship: who produced a message is a
separate question from which side it sits on: pass
authoron/turnandadditional_metadata.authoron the store — identity as ordinary metadata, surfaced as the chat message'snamefield in compiles and recalls. Multi-agent transcripts stay attributed, and it filters:/recall_with_metadatawith{"author": ...}. - Turn-local instructions: where content enters
decides how long it lives.
artifacts_contextis included in one compile and never stored.operator_briefingis also compiled-only, but re-send it every turn — standing instructions and mode flags.holdsis a list of{id, title, content}objects you keep client-side and pass on every/turnuntil you drop them — pinned operational state ("stop-order", "API is down") that never enters memory: not stored, not recalled, not in receipts. Durable facts go to/create_memory— forever, recallable from any session. Pick the shortest lifetime that does the job; anything longer persists into future context where it no longer belongs. - Context meter: read your gauge from the response's
token_countagainstbudget_total— the compiled packet's own numbers. Local history length undercounts (it knows nothing about recall); providerusagemeasures a different tokenizer after your own additions. - Control tokens: Hivemind stores and returns text
verbatim — but recalled text re-enters your model's context later, and
that model's tokenizer may remove its own special tokens or interpret
them as control sequences. Strip or alter markers like
<|...|>before storing. - Failure severities differ. A 429 carries
Retry-After— sleep it and retry. A 402 storage cap blocks writes but recall and compile keep working — don't fail the chat. A failed/conversation/storemeans the chat already succeeded — retry it out-of-band rather than surfacing an error to the user. Never re-send a/turnthat timed out: the request may have executed, and replaying double-stores the message.
Projects and storage
Your memory lives in a Qdrant cluster you own — we
never hold your data. After subscribing, connect a cluster from the
dashboard: paste its URL and API key,
we validate it live (reachable, key accepted, collection created with
its indexes) and requests start working within minutes. Until a cluster
is connected, every request answers 400 with
"No storage connected". Any Qdrant instance the service can
reach works: Qdrant
is open source, so you can self-host it on your own server or VPS
and pay nobody for storage, or use
Qdrant Cloud if you'd
rather not operate it — its free tier is enough to start. Every turn
round-trips to your cluster, so distance between the service and your
cluster is latency, and your cluster's capacity sets your throughput.
If your cluster is unreachable, requests fail closed rather than
writing anywhere else.
A workspace holds multiple projects, each bound to
one cluster (the same cluster or different ones). Projects are hard data
separation: each resolves to its own storage, so records in one project
are never candidates for recall in another — separate agents,
environments, or customers of yours can share a workspace without
sharing memory. Requests select a project with the
X-Hivemind-Project header; without it, your default project
serves. Credentials can be rotated and
clusters moved per project at any time from the dashboard.
Errors and limits
| Status | Meaning & what's in details |
|---|---|
404 MEMORY_NOT_FOUND | Nothing relevant stored yet — normal for a new workspace; proceed and store what you learn. |
402 STORAGE_LIMIT_EXCEEDED | Storage allowance reached — only when a subscription carries one. Your memory lives in your own Qdrant cluster, which has no allowance imposed by us; its capacity is yours to manage. Where a limit applies it is writes only — recall and compile keep working, so a capped agent degrades to read-only memory instead of losing recall entirely. details carries max_storage_bytes and used_storage_bytes |
429 RATE_LIMITED | Per-minute, per-workspace limit hit. The Retry-After header says exactly how long to sleep — use it instead of guessing a backoff |
400 VALIDATION_ERROR | Malformed request: empty content, missing session id, an explicit recall_top_k above your plan's ceiling, or a budget_total above 128,000. details.field names the offending field. Also "No storage connected" (details.action = connect_cluster): your workspace is active but no Qdrant cluster is connected yet — connect one on the dashboard |
401 UNAUTHORIZED | Missing/invalid/revoked key, or an operation your subscription doesn't enable — the message names which. Key revocations propagate within ~5 minutes |
5xx / timeout | Service or storage trouble. Your call whether to fail open (proceed with local context only) or fail closed — decide once, document it in your host |
All errors arrive as one machine-readable envelope shape:
{"ok": false, "error": {"code", "message", "details"}, "request_id"}.
The code values are stable strings (branch on them, not on
messages), and request_id is the correlator to quote in a
support request — it ties your call to our logs and receipts.