Introduction
Ijima is the centralized memory service for the Anima ecosystem — the single source of truth for agentic memory across every harness (pi, Tsume, Sakamoto, Wallace, Dominic, opencode, …). It replaces fragmented, per-harness memory stores with one service every agent reads from and writes to, with per-principal isolation, quantitative access control, and full provenance on every stored fact.
What it does
Ijima serves three related capabilities:
- A memory palace — curated memories organized as project → topic rooms, with importance-weighted recall, semantic search, and a diary per agent.
- A knowledge graph — subject/predicate/object triples with entity resolution, timelines, and cross-project tunnels.
- A session repository + mining pipeline — raw agent session turns flow in, an extraction engine (rules tier, optional LLM tier) proposes candidate memories with confidence scores, and a human/agent review queue promotes the good ones into the palace with provenance intact.
Why it exists
Before Ijima, each harness carried its own memory: pi-mempalace SQLite databases per workstation, ZeroClaw’s Discord brain, ad-hoc files. Memory was fragmented (the same insight saved five times, never reconciled), unauditable (no notion of where a fact came from or how much to trust it), and unsecurable (one all-or-nothing bearer token). Ijima makes memory a service: one instance, many principals, capability-scoped access, and a trust tier on every entry.
Ecosystem position
Ijima is the memory plane under Dominic’s orchestration plane:
- Schubert provides the capability algebra (Grassmannian Gr(4,8)) that Ijima uses for authorization, proof-carrying GrantTokens, and rate limiting. Ijima is Schubert’s first and most complete consumer.
- Dominic (meta-orchestrator) dispatches work and federates through Ijima’s control plane; Ijima enforces trust boundaries locally even when Dominic is unreachable.
- pi connects as a thin client (
IJIMA_URL/IJIMA_TOKEN), replacing the pi-mempalace extension. - Proserpina supplies the LLM agent surface for the mining tier
(
proserpina-agent), cross-repo contract-tested.
The workspace
| Crate | Role |
|---|---|
ijima-core | Domain types, Store/KnowledgeGraph traits, capability vocabulary |
ijima-server | HTTP daemon (axum), SurrealDB backend, auth, CLI |
ijima-client | Typed async HTTP client for harnesses |
ijima-miner | Extraction engine (rules + LLM tiers) |
ijima-pi / integrations/pi | pi extension (WASM/npm) |
Ijima is v0.x: interfaces evolve, breaking changes are announced in the CHANGELOG, and the 0.2.0 “Central Brain” release targets a real single-instance deployment with all workstations as thin clients.
Getting Started
Prerequisites
- A recent Rust toolchain (see
rust-toolchain.toml). - (Optional) a model endpoint for the mining LLM tier — DeepSeek by default.
- (Optional) a Tailscale network for remote thin clients.
Install
Ijima is published on crates.io:
cargo install ijima-server --features "cli,backend-sqlite,embeddings-candle"
The default features give you the daemon, HTTP, Schubert auth, and the
SurrealDB backend. Add backend-sqlite if you will import from a legacy
pi-mempalace or ZeroClaw database, and embeddings-candle for semantic
search (downloads all-MiniLM-L6-v2 on first use).
Start a daemon
ijima serve
The daemon listens on 127.0.0.1:7373, creates a data directory at
~/.ijima (an embedded SurrealDB store + issuer key), and is ready.
IJIMA_DIR relocates the data directory; see
Installing and Configuring.
Mint a grant
Every request needs a Schubert GrantToken. Mint one for yourself:
ijima token issue --principal elliott \
--capabilities memory:read,memory:write,knowledge:read,knowledge:write
The token is a base64 GrantToken blob — proof-carrying, signed by the issuer key in the data directory. Store it somewhere safe (a password manager); it is a credential.
First requests
TOKEN="..." # from the step above
# store a memory
curl -s -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{"id":"mem_first","content":"Ijima is running","project":"ijima",
"topic":"notes","source":"Explicit","harness":"Pi",
"importance":0.5,"created_at":"0"}' \
localhost:7373/memories
# recall it
curl -s -H "Authorization: Bearer $TOKEN" \
localhost:7373/memories/mem_first
The response provenance block reports what the daemon saw: source tier, harness, origin instance, authority scope.
From a harness
Rust harnesses use ijima-client:
#![allow(unused)]
fn main() {
let client = ijima_client::Client::new(
ijima_client::ClientConfig::new("http://127.0.0.1:7373", Harness::Pi)
.with_token(token),
);
let id = client.store_memory(memory).await?;
}
pi users: the pi integration is a single env pair
(IJIMA_URL, IJIMA_TOKEN).
Next steps
- Concepts — the two-store model, provenance, capabilities.
- Importing Legacy Corpora — bring your pi-mempalace history in.
- The Mining Pipeline — turn session transcripts into memories.
The Two-Store Model
Ijima serves two related but distinct stores, unified by a miner that turns one into the other.
1. The Memory Palace
Long-term, curated, semantic memory. Verbatim storage + candle embeddings +
cosine search + a temporal knowledge graph (entities and triples with
valid_from/valid_to). This is the pi-mempalace model, production-proven,
reimplemented in Rust and import-compatible with its schema.
Palace entries always carry provenance: source tier, harness, session, origin instance, and authority scope — so any entry traces back to the conversation that produced it.
2. The Session Context Repository
Raw session transcripts from every harness: every conversation, every agent run, every gateway exchange. Append-only, high-fidelity, uncensored at write time. Not curated, not summarized — the raw ore.
The Miner: ore → metal
The novel capability. Ijima mines raw sessions to extract curated palace entries:
- Decisions — “we decided to use DeepSeek.”
- Facts — “Ijima depends on candle.”
- References — URLs, citations, file paths.
- Patterns — recurring topics, open threads.
A rules tier runs unconditionally (no model); an optional LLM tier (Proserpina) adds Fact + Pattern roles. Low-confidence extractions stage in a per-namespace review queue rather than auto-archiving. See The Mining Pipeline.
raw sessions ──▶ [ rules + llm ] ──▶ Auto (palace) / PendingReview (queue)
The raw session always remains in the repository for full-fidelity recall, even after mining.
Namespaces & Multi-Tenancy
Every request to Ijima is scoped to a namespace — the isolation unit. No request spans namespaces; there is no implicit cross-namespace read.
Namespace classes
| Class | Shape | Who sees it |
|---|---|---|
| Personal | ns_<principal>_private | The principal only |
| Shared | ns_<org>_shared (e.g. ns_ia_shared) | Members (membership-gated) |
| Import | ns_import_<source> | Imported corpus staging, per source |
| Global commons | global | Legacy migration baseline; readable |
Routing
- Omit the namespace → your personal namespace.
- Pass
?namespace=<ns>→ that namespace, subject to checks: another principal’s*_privatenamespace is always forbidden, and shared namespaces require membership (granted by an admin; admins bypass). - Open namespaces —
global,ns_doctrine,ns_import_*staging — are readable/writable by any authenticated principal. - Curated corpora land in org-scoped doctrine walls (
ns_<org>_doctrine):POST /doctrine?namespace=(admin, or the narrowdoctrine:writecapability for unattended ingest timers) upserts into the wall with stable path-derived ids; the instance-globalns_doctrinestays admin-only. Same membership rules as any wall for reads. - Writes require
memory:write(orknowledge:write) and namespace eligibility; reads are personal-by-default and explicit otherwise. - Promotion targets go through the same rule (
trust:promotecannot tunnel through an org wall; import staging is not a promotion target).
Shared-namespace membership (org walls like ns_ia_shared,
ns_kellas_shared) is runtime-managed store data, not static policy —
grants take effect immediately for principals holding valid grants:
ijima namespace grant ns_ia_shared elliott --auth <admin-bearer>
ijima namespace members ns_ia_shared --auth <admin-bearer>
ijima namespace revoke ns_ia_shared elliott --auth <admin-bearer>
Admins bypass membership checks (operator access). See the namespace-membership ADR.
Isolation mechanics
Isolation is enforced at two layers:
- API layer —
resolve_nsrejects foreign private namespaces before the store is touched. - Store layer — SurrealDB record keys are namespaced composites
(
<namespace>:<memory-id>), so the same logical id can exist in two namespaces without collision. This matters for imports: the same pi-mempalace row imported from two workstations coexists per-source.
Import namespaces
ijima import defaults each source to ns_import_<sanitized-source> —
never the global commons. A source name like Laptop 01 becomes
ns_import_laptop_01. Import namespaces are staging areas: content lands
at the AutoCapture trust tier and is promoted into personal/shared
namespaces only after review (see Provenance).
Namespace-aware surfaces
The palace features mirror the memory store’s scoping: rooms and taxonomy
are computed per namespace; the knowledge graph is namespaced per
triple/entity; sessions and diaries carry their namespace. The
/palace/graph and /tunnel traversals operate within one namespace’s
view by design.
What a search “sees”
POST /memories/search takes a scope:
personal(default): the resolved namespace only — the caller’s private namespace, or the?namespace=override.visible: the principal’s readable world — own private + theglobalcommons + openns_import_*staging + every org wall they hold membership in, merged and ranked by similarity across all of them. The pi extension searchesvisibleso a fresh session finds the whole brain, not just its own (initially empty) namespace.
Membership still gates: a wall the principal is absent from never
appears in visible results.
Agent homes
Personal-by-default is right for private notes — and exactly wrong for
fleet-institutional knowledge, which fragments across per-host silos
where sibling agents can’t recall it. Agent homes (0.2.5) point the
default the other way: clients configured with a home namespace
(IJIMA_NAMESPACE, typically an org wall like ns_ia_shared) capture,
save, dedup-check, and wake up in that namespace.
The rules compose cleanly:
- Captures/saves in the home follow the wall’s membership — every member agent writes to and reads from one shared pool.
- Wake-up essentials come from the home: a memory saved on host A greets the next session on host B.
- Another org’s agents set their own wall as home; membership gating applies as always. Homes never widen access — they relocate the default.
- Unset home = personal namespace: private-by-default stays available for anything that genuinely is per-principal.
Provenance & Trust Tiers
Every memory in Ijima carries a provenance block. Provenance is not metadata garnish — it is the basis for trust decisions, promotion, and (eventually) federation conflict resolution.
The provenance fields
| Field | Meaning |
|---|---|
source | Trust tier: Explicit, AutoCapture, Mined, or Doctrine |
harness | Which harness wrote it (Pi, Dominic, Wallace, …) |
origin | The instance that authored the entry |
authority | Source-of-truth scope for the entry’s domain |
session_id | Originating session, when known |
Trust tiers
Explicit— an operator or harness deliberately saved it. Highest routine trust.AutoCapture— an automatic hook wrote it. Unverified.Mined— extracted from a session transcript by the miner, carrying a confidence score until reviewed.Doctrine— curated, Git-versioned, PR-reviewed memory mirrored; since 0.3.0 it can live in org-scoped walls (POST /doctrine?namespace=), keeping internal corpora out of the instance-global namespace. from the repository seed pack. Never written directly by agents.
Trust transitions are themselves capabilities: trust:promote raises an
entry’s tier, and cross-tier endorsement/override are progressively more
expensive in the capability algebra (see
Capabilities). Raising trust costs more than writing
at a tier — by construction.
The evidence axis (0.4)
Provenance answers who wrote it; evidence grade answers how they
know it. They are independent axes — the grade crosses tier lines. An
Explicit save by an operator can still be an interpretation, and a
low-tier AutoCapture row can carry a directly observed fact.
| Axis | Question | Values |
|---|---|---|
Provenance tier (source) | Who (or what) wrote this? | Explicit, AutoCapture, Mined, Doctrine |
Evidence grade (evidence) | How does the author know it? | Observed, Interpreted |
Interpretedis the default. Inference, judgment, summary, and any ungraded save land here — including every legacy row written before 0.4.0 and every save that never set a grade.Observedmeans the authoring process directly witnessed the thing itself — a session transcript event, command output, or a git artifact. An observed claim must cite (citations>= 1): no citation, no observed claim. The server rejectsObservedwithout citations at save time (POST /memories→400).
Citations are typed pointers (kind ∈ Commit, Report, Session,
File, Url; locator opaque) to the artifact that grounds the claim.
A grade is a claim about knowledge, not a credential — raising the
provenance tier does not by itself make an entry observed.
Imports land unverified
ijima import stamps every imported memory origin = <source> and drops
the tier to AutoCapture regardless of its original classification —
a manual-save row from a workstation’s pi-mempalace arrives as
AutoCapture. Imported content is unverified until promoted through the
review path. This is deliberate: an import is a claim, not a credential.
Why authority matters
authority records whose fact this is — the local instance, or a
remote instance’s scope. In the single-instance present it is uniformly
local; when federation lands, per-domain authority scopes drive
cross-instance conflict resolution (the instance whose authority scope
matches a domain wins that domain’s writes).
Corrections: supersede
Since 0.4.0.
The memory palace treats correction as a first-class structural
operation, not a ranking fight. Before 0.4, a wrong high-importance
memory outranked every lower-tier correction forever — wake-up orders
lexicographically (importance DESC, created_at DESC), so recency
never closed an importance gap, and corrections only worked when a
human re-saved them at matching importance to win the recency
tiebreak. The 2026-09-22 research dive called this “a ritual, not a
mechanism.” Supersede is the mechanism.
The link
Saving a memory that declares supersedes: <target_id>:
- writes the inverse link (
superseded_by,superseded_at_unix) onto the target as part of the save — serialized and failure-compensated (see below); - excludes the target from wake-up, search, and browse — regardless of either row’s importance, tier, or age;
- keeps the target recallable by id, with its links intact — superseded rows are visible fossils, not deletions.
POST /memories
{
"id": "mem_correction",
"content": "The wire shape was fixed in commit abc123 — earlier notes were wrong.",
"supersedes": "mem_wrong_note",
"evidence": "Observed",
"citations": [{ "kind": "Commit", "locator": "abc123" }],
...full Memory fields...
}
Semantics
- Chains, not re-targeting: a memory that is already superseded
cannot be superseded again — supersede the successor.
A ← B ← Cis a valid chain;A ← BthenA ← Cis rejected (409). - Self-supersede is rejected (409); a target absent from the namespace is 404.
- Namespace-scoped: the link resolution never crosses walls.
- Concurrency-safe (the precise guarantee): a supersede save is
insert → conditional claim → compensate, serialized per process —
not a single transaction. The claim on the target is one conditional
atomic update (
UPDATE … WHERE superseded_by = NONE), so concurrent successors of one target yield exactly one winner; a claim that fails (absent, claimed, or self target) compensates by deleting the just-inserted successor, leaving the pre-attempt graph. Mutually referencing saves see the serial outcome — both rejected and compensated away — instead of forming a correction cycle. The correction graph is acyclic by construction under single-process operation; cross-process serialization is federation’s problem.
Where exclusion applies
| Path | Superseded rows |
|---|---|
| Wake-up (personal essentials) | excluded |
| Semantic search | excluded |
| Browse / project-topic listing | excluded |
| Recall by id | included (with links) |
Content dedup (/memories/check, save-path) | excluded — a retired row’s content does not block a fresh save |
| Doctrine cross-id dedup | never canonical; stale duplicates retire via supersede links |
Corrections and evidence grades
A correction that merely asserts a different claim is an
interpretation. A correction that cites what it observed
("Observed" + citations — see provenance) carries
the evidence axis with it. The two mechanisms compose: the supersede
link decides what is visible; the evidence grade declares how the
replacement knows.
Doctrine is versioned, not superseded
The doctrine tier (Git-reviewed, ingested from corpus) corrects itself through versioning and rollback, not supersede links — its rows carry stable revision identities instead. The two correction systems are tier-appropriate: agent memory displaces; doctrine re-versions.
The retrieval ledger
Since 0.4.2. The ledger records what the read paths actually serve.
Every wake-up, search, and browse response appends one event per served memory to an append-only ledger. The events carry ids and metadata only — never memory content and never query text.
Why
Before 0.4.2 the read paths had no telemetry at all. Which memories reach agent context, how often, from which wall, and — just as important — which never do: these were unanswerable after the fact. Policy questions (is the recency stratum admitting too much? does visible search drown small walls?) need data before they need opinions. Instrumentation before policy.
The event
| Field | Meaning |
|---|---|
retrieval_id | Groups all events from one request |
kind | WakeupRecent, WakeupLexicographic, WakeupDoctrine, Search, Browse |
namespace | The wall the memory was served from |
memory_id | Join key back to the palace |
principal | Who triggered the retrieval |
rank | 0-based position in the served list |
similarity | Cosine similarity (Search events only) |
ts_unix | Unix seconds |
Wake-up strata are recorded separately: the recency stratum (admitted first, the starvation fix) and the lexicographic fill (importance order) tag their events distinctly, so stratum distribution is a one-query question.
Reading it
Admin-gated:
# newest 100 events on one wall
curl -H "authorization: Bearer $ADMIN" \
"http://127.0.0.1:7373/retrieval/ledger?namespace=ns_shared&limit=100"
# aggregate counts
curl -H "authorization: Bearer $ADMIN" \
"http://127.0.0.1:7373/retrieval/ledger/stats?namespace=ns_shared"
# prune older than a timestamp (returns count deleted)
curl -X DELETE -H "authorization: Bearer $ADMIN" \
"http://127.0.0.1:7373/retrieval/ledger?before_unix=1770000000"
Design notes
- Fire-and-logged. An append failure logs a warning and the read still succeeds — observability must not become a new failure mode.
- Ids, not content. The ledger joins back to the palace by
memory_id; it never duplicates content or query text. - Append-only until pruned. No process rewrites events; pruning is an explicit admin action with a returned count.
Capabilities & GrantTokens
Ijima’s access model is Schubert capability algebra on the Grassmannian Gr(4,8). Capabilities are not role strings — they are Schubert conditions (subspaces), and access decisions are geometry: a grant authorizes an action when the grant’s subspace intersects the capability’s.
The vocabulary
Eleven capabilities, each a partition on Gr(4,8):
| Capability | Codimension | Governs |
|---|---|---|
memory:read | 1 | Read/recall/search memories |
knowledge:read | 1 | Query the knowledge graph |
mining:review | 2 | Read the review queue |
memory:write | 2 | Store/delete memories, diary |
knowledge:write | 2 | Write triples |
session:ingest | 3 | Ingest session turns |
doctrine:write | 3 | Ingest doctrine into explicitly-targeted walls (global stays admin-only) |
mining:trigger | 3+1 | Trigger mining runs |
trust:promote | 3+1 | Raise a memory’s trust tier |
trust:endorse | 4+1 | Cross-tier endorsement |
trust:override | 4+2 | Authority override (rare) |
admin | 4,4,4,4 (point class) | Token admin, status, repos |
Write implies read in the geometry: memory:write’s partition
contains memory:read’s, so a write grant can also read. A read grant
can never write.
GrantTokens
A GrantToken is a compact, proof-carrying, ed25519-signed token that bundles one or more capabilities for one principal:
ijima token issue --principal sara \
--capabilities memory:read,memory:write,knowledge:read
- Multi-capability — one grant covers a whole job description; no more per-capability token bundles.
- Verified geometrically — the daemon decodes the grant, verifies the signature, and checks each request’s capability against the grant’s partition set.
- Partition-signed — the capability list is inside the signed blob; clients cannot edit grants.
- Revocable — see Token Management.
Why geometry
The codimension of a capability is a quantitative authorization weight —
and it doubles as the rate-limit capacity (intersection-number-scaled
token buckets). Low-stakes capabilities (memory:read, codim 1) are
cheap; trust-flow capabilities (trust:override) are expensive; admin
is the point class. The geometry of access is also the geometry of
throughput, and trust-flow costs more than access — deliberately.
The vocabulary is declarative (policy.toml in the daemon crate); adding
a capability is a policy edit plus a partition assignment, not a code
change.
Sessions & the Miner
The session repository is Ijima’s raw-material intake; the mining pipeline turns it into curated memory. This is Ijima’s most distinctive loop: raw conversation in one end, reviewed, provenance-carrying memory out the other.
The flow
harness ──POST /sessions/:id/turns──▶ session repository (verbatim)
│
mining:trigger│
▼
extraction engine
(rules tier → LLM tier)
│
Auto-confidence ───┼─── PendingReview
(auto-filed) │
▼
review queue
│ mining:review
▼
accept → palace (Mined tier)
reject → archived
- Ingest (
session:ingest) — harnesses stream turns verbatim. No filtering, no interpretation: the repository is the audit record. - Extract (
mining:trigger) — the extraction engine runs over a session. The rules tier (deterministic, cheap) catches decisions, TODOs, and entity mentions; the LLM tier (optional, via a Proserpina HTTP agent) proposes richer candidates with confidence scores. - Route by confidence — high-confidence extractions file
automatically as
Minedmemories; the rest wait in the review queue. - Review (
mining:review) — a human or agent reviews the queue, accepting (→ palace, provenanceMined+ source session) or rejecting (→ archived, kept for the record).
Why sessions stay verbatim
The repository is deliberately unprocessed: mining proposals can be rejected, improved, and re-run, but the source transcript is immutable evidence. Provenance on every mined memory points back at the session and turn range it came from.
Trust flow
Mined memories enter at the Mined tier — above nothing, below
Explicit. Promoting them (or any memory) to higher trust is a separate,
deliberate act gated by trust:promote. The pipeline never silently
raises trust.
See The Mining Pipeline for operation, and the miner architecture ADR for the tier design.
Installing and Configuring
Configuration precedence
Ijima resolves settings in a strict layer order — later wins:
defaults < ijima.toml file < environment variables < CLI flags
Config file discovery
The daemon looks for ijima.toml in order:
$IJIMA_CONFIG— explicit pointer (a missing or malformed file at an explicit pointer is a hard error, not a silent fallback)$IJIMA_DIR/ijima.toml/etc/ijima/ijima.toml
Config keys
# /etc/ijima/ijima.toml
host = "127.0.0.1"
port = 7373
data_dir = "/var/lib/ijima"
issuer_key = "/var/lib/ijima/issuer.key"
rate_base = 10
rate_multiplier = 1.0
embedding_model = "sentence-transformers/all-MiniLM-L6-v2"
Unknown keys are ignored (forward compatibility).
Environment variables
| Variable | Purpose |
|---|---|
IJIMA_DIR | Data directory (default ~/.ijima) |
IJIMA_CONFIG | Explicit config file path |
IJIMA_POLICY | Issuance-policy file (policy-constrained ijima token issue; see Token Management) |
IJIMA_HOST / IJIMA_PORT | Bind address overrides |
IJIMA_KEY | Issuer key path override |
IJIMA_RATE_BASE / IJIMA_RATE_MULTIPLIER / IJIMA_RATE_DISABLE | Rate-limit tuning |
IJIMA_TLS_CERT / IJIMA_TLS_KEY | PEM paths for the tls feature |
IJIMA_EMBED_MODEL / IJIMA_EMBED_REVISION | Embedding model pinning |
IJIMA_LLM_MODEL / IJIMA_LLM_BASE_URL / IJIMA_LLM_API_KEY | Mining LLM tier endpoint |
IJIMA_INSTANCE_ID / IJIMA_INSTANCE_ROLE / IJIMA_INSTANCE_SCOPES | Federation instance identity |
IJIMA_LOG | Log filter |
Client-side (thin clients and the CLI’s remote commands): IJIMA_URL
(default http://127.0.0.1:7373) and IJIMA_TOKEN (the bearer grant).
Feature selection at install time
cargo install ijima-server \
--features "cli,backend-sqlite,embeddings-candle,mining,tls"
See Feature Flags for the full matrix. The default
set (std,http,server-auth,backend-surreal) runs a production daemon;
add backend-sqlite only for one-time imports (it exists to read legacy
databases).
Running the Daemon
Foreground
ijima serve # 127.0.0.1:7373, data at ~/.ijima
ijima serve --host 0.0.0.0 --port 7373
IJIMA_DIR=/var/lib/ijima ijima serve
systemd (production)
The repository ships a hardened unit at
deploy/ijima.service
plus a commented
deploy/ijima.toml.example:
sudo install -m644 deploy/ijima.toml.example /etc/ijima/ijima.toml
sudo install -m644 deploy/ijima.service /etc/systemd/system/
sudo systemctl enable --now ijima
The unit runs with a dedicated user, ProtectSystem=strict,
ReadWritePaths=/var/lib/ijima, and Restart=on-failure.
Observability
GET /health— liveness (no auth).GET /status— memory/namespace/entity/triple counts, version, start time, uptime (admin).- Structured logs via
tracing(IJIMA_LOGfilter;RUST_LOGalso respected by convention).
Lifecycle (AutoCapture TTL)
A daily background sweep deletes source = AutoCapture memories
older than the TTL — tier-gated by construction: Explicit,
Doctrine, Mined, and imported rows never age out, however old.
Only ambient chatter expires.
| Knob | Default |
|---|---|
config autocapture_ttl_days | 30 |
env IJIMA_AUTOCAPTURE_TTL_DAYS | overrides config |
0 | disables the sweeper |
First pass runs ~10s after boot (so restart cycles catch up), then
every 24h. Deleted counts land in the journal (autocapture ttl sweep) — soak-log evidence by design.
TLS
With the tls feature, set IJIMA_TLS_CERT and IJIMA_TLS_KEY (PEM
paths) — the daemon binds HTTPS via axum-server/rustls. On a private
Tailscale network, tailscale serve in front of plain HTTP is the
documented alternative.
Restart semantics
The SurrealDB (surrealkv) engine holds a directory LOCK while open.
Dropping the in-process handle does not release it synchronously —
background engine tasks must wind down. This is invisible across process
boundaries (the OS releases the lock at exit), which is the normal
restart path for the daemon. Only embedders opening/closing the same
data directory sequentially inside one process need the spawn-and-yield
pattern (documented on SurrealStore::open_persistent).
Upgrades
Stop the daemon, replace the binary, start. The store’s schema is defined
idempotently at open (DEFINE TABLE IF NOT EXISTS / DEFINE INDEX IF NOT EXISTS), so minor upgrades with an unchanged on-disk layout need no
migration step. Pre-0.2 development databases (pre-namespaced record
keys) should be re-imported rather than carried forward — see
Importing Legacy Corpora.
Token Management
Grants are credentials. This chapter is the operator’s lifecycle guide.
Issuing
Issuance is policy-constrained (Schubert 0.5 #20.3): ijima token issue signs only what the issuance policy entitles, failing closed on
unknown principals and over-entitled requests. The policy resolves as
--policy PATH > $IJIMA_POLICY > $IJIMA_DIR/policy.toml > the
embedded default — which seeds no principals, so a fresh install
mints nothing until the operator provisions a policy file (bootstrap
below).
The operator file can be a minimal principals-only overlay:
# $IJIMA_DIR/policy.toml
[principals.elliott]
grants = ["memory:read", "memory:write", "knowledge:read", "knowledge:write"]
[principals.minoru]
grants = ["session:ingest", "mining:review"]
Partitions always derive from the embedded policy — an overlay can assign capabilities but never redefine the geometry. Adding a workstation = one file edit; no rebuild, no daemon restart (the daemon verifies proof-carrying grants, not principals).
# a full personal grant
ijima token issue --principal elliott \
--capabilities memory:read,memory:write,knowledge:read,knowledge:write
# a machine feed grant — always expiring
ijima token issue --principal minoru \
--capabilities session:ingest,mining:review \
--expires-in 2592000 --json # 30 days
# an operator/admin grant (rare; the point class)
ijima token issue --principal ops --capability admin --json
--json emits {token, principal, capabilities, expires_at_unix?, public_key}. --expires-in <SECONDS> sets a signed expiry (Schubert
0.5 ADR-0001; boundary inclusive — dead when now >= expires_at);
omit for never-expiring. Renewal = re-issue (fresh nonce), never
mutation.
Deploying to clients
Thin clients need two env vars:
export IJIMA_URL="http://ijima.tailnet:7373"
export IJIMA_TOKEN="<grant blob>"
The pi extension, ijima-client, and the CLI’s remote subcommands all
honor them.
Revoking
The kill-switch for leaked or rotated credentials:
ijima token revoke --token "<bearer>" \
--url http://127.0.0.1:7373 --auth "<admin-bearer>" \
--reason "leaked in CI log"
- Revocation is store-backed and survives restarts: the daemon persists a SHA-256 hash of the bearer and hydrates an in-memory set at boot. Raw bearer values never touch the store, logs, or backups.
- The check composes with signature verification: a revoked bearer is rejected even though its signature is valid.
- Past revocations:
ijima token revocations --auth "<admin-bearer>"(admin), oldest first.
Rotation practice
- Prefer short expiries + re-issue for machine feeds (
--expires-in);revokefor incidents; issuer-key rotation remains the emergency lever for key compromise (invalidates every grant at once). - Schubert 0.5 expiry is adopted; the instance-side revocation list stays as defense-in-depth (kills leaked bearers with no issuer involvement). CRDT nonce-tombstones arrive with 0.3 satellites.
Importing Legacy Corpora
ijima import streams an external SQLite corpus into a running daemon
over HTTP — the files never leave the workstation, the daemon does the
storing. This is the 0.2.0 path for consolidating pi-mempalace (and
ZeroClaw) history into the central instance.
Usage
ijima import mempalace --db ~/.pi/agent/mempalace/memories.db \
--source "elliotthall-laptop"
ijima import zeroclaw --db ~/zeroclaw/brain.db --source "zeroclaw-archive"
| Flag | Meaning |
|---|---|
--db PATH | Source SQLite database |
--source NAME | Provenance origin stamp + namespace derivation |
--namespace NS | Override the target namespace |
--url URL | Daemon (default $IJIMA_URL or 127.0.0.1:7373) |
--token TOKEN | A memory:write grant (default $IJIMA_TOKEN) |
What the importer does
- Reads the source rows (memories; pi-mempalace corpora also carry
entities+triplesknowledge-graph tables — see step 5). - Retags provenance:
origin = <source>, trust tier dropped toAutoCaptureregardless of original classification. Harness provenance is preserved (Pifor mempalace,Otherfor ZeroClaw). - Routes into
ns_import_<sanitized-source>(e.g.Laptop 01→ns_import_laptop_01) — never the global commons.--namespaceoverrides for deliberate shared targets. - Dedups: every memory is pre-checked via
POST /memories/check(content-hash) before storing. The same memory saved on two workstations stores once per source namespace; re-running an import is idempotent. If the daemon rate-limits the import (HTTP 429) the client backs off exponentially (250 ms doubling, ~16 s budget) and retries — rate limiting never drops rows. - Imports the knowledge graph (pi-mempalace sources only):
entities are re-addressed from opaque
ent_*hashes to Ijima’s id-is-name convention (same-name source entities merge), and each triple is added with its confidence + temporal range — a sourcevalid_tois applied as an invalidation so historical facts stay historical. Triples referencing unknown entities are counted asunmapped, never imported. - Reports per source:
{
"memories": { "attempted": 14459, "added": 14459, "deduped": 0, "skipped": 0 },
"knowledge": { "attempted": 124, "added": 124, "skipped": 0 },
"unmapped": 0
}
skipped counts per-row failures — one bad row never aborts the run.
After import
Import namespaces are staging. Review the content and promote what you
trust into personal or shared namespaces (trust:promote); the origin
stamp (elliotthall-laptop) travels with every promoted memory, so the
workstation trail survives.
The older migrate path
ijima migrate --palace <db> is the pre-HTTP one-shot local import (runs
against the daemon’s own data directory, no HTTP). Prefer import — it
works against remote daemons, dedups per-source, and tags provenance
properly.
The Mining Pipeline
Ijima’s novel capability: turning raw session transcripts into curated memory palace entries with full provenance.
Tiers
- Rules tier (always on, no model): deterministic extraction of
Decisions (“we decided to…”) and References (URLs /
scheme:links). Fast, free, side-effect-free. - LLM tier (optional,
miningfeature): Proserpina-backed Fact and Pattern roles. Single-shot per role (no panel cross-examination in v0). Emits one JSON object per line:{"content","project","topic","confidence"}.
Configure the LLM tier with IJIMA_LLM_BASE_URL / IJIMA_LLM_MODEL /
IJIMA_LLM_API_KEY. When model/key are unset, mining runs rules-only.
Routing
- Auto extractions archive straight to the palace (content-hash dedup applies).
- PendingReview extractions stage in a per-namespace review queue.
Confidence ≥ 0.85 overrides a role’s default to
Auto— a high-confidence fact auto-archives.
Trigger
# Mine a session, then review what landed in the queue.
curl -X POST http://127.0.0.1:7373/sessions/sess_1/mine \
-H "authorization: Bearer <mining:trigger-token>"
curl http://127.0.0.1:7373/mining/queue \
-H "authorization: Bearer <mining:review-token>"
curl -X POST http://127.0.0.1:7373/mining/queue/<id>/accept \
-H "authorization: Bearer <mining:review-token>"
Architecture notes
Extraction is pure and synchronous; a separate async ingest step writes
to the store. Because the Proserpina HttpAgent::respond blocks on its own
tokio runtime, the daemon runs the sync mine_all pass inside
spawn_blocking — owning the concrete (Send) agent and coercing to
&mut dyn Agent inside the closure. See
docs/adr/miner-architecture.md.
pi Thin-Client Integration
pi (the coding-agent harness) talks to Ijima as a thin client — no local
store, no local daemon. The extension is @industrialalgebra/ijima-pi
(npm), built from the ijima-pi crate compiled to WASM.
Setup
Three environment variables — only the token is required:
| Variable | Meaning | Fallback / default |
|---|---|---|
IJIMA_TOKEN | A multi-capability grant (below) | IJIMA_TOKEN_FILE → ~/.config/ijima/token |
IJIMA_URL | Daemon base URL | IJIMA_URL_FILE → ~/.config/ijima/url → http://127.0.0.1:7373 |
IJIMA_NAMESPACE | Home namespace (see below) | .pi/ijima.json namespace (see below) → personal |
IJIMA_HOME | Overrides ~ expansion (containers, tests) | the real home directory |
Precedence is always environment → project file (.pi/ijima.json, nearest
walking up from the session root) → default — for all three connection
knobs (namespace, url, token_file). The file may also set url and
token_file directly.
Mint the grant on the daemon:
ijima token issue --principal pi-workstation \
--capabilities memory:read,memory:write,knowledge:read,knowledge:write
The file fallbacks mean a host needs no shell configuration at all — drop the token (and optionally the url) file in place and every process (agent tabs, cron, systemd units, non-login shells) just works.
Agent homes
By default a principal’s memories land in its personal namespace — private, invisible to siblings. For fleet-institutional knowledge that is exactly wrong: set the home to your org wall and every member agent shares one pool:
export IJIMA_NAMESPACE="ns_ia_shared" # this org's wall
Captures, saves, dedup checks, and wake-up all operate in the home
namespace — a memory saved on one host greets the next session on every
other member host. A separate org’s machine sets its own wall instead
(ns_other_org_shared); membership gating applies as usual, so walls
keep meaning what walls mean. Unset = personal, private by default.
(Namespaced wake-up requires daemon ≥ 0.2.5; captures work on 0.2.2+.)
The declarative form: .pi/ijima.json (0.3.1+)
Instead of exporting the variable per shell (or per systemd unit), a repository can declare its wall — put this at the project root:
{ "namespace": "ns_orthant_shared" }
The extension reads the nearest .pi/ijima.json walking up from the
session root (nearest wins, so a monorepo can override a parent). Optional
url and token_file keys pin the daemon and credentials the same way.
Precedence is always environment → project file → default, so an
exported IJIMA_NAMESPACE overrides the declaration — and an org repo
cloned anywhere lands in its wall on first launch, no shell configuration
at all. This is the recommended form for multi-org fleets.
The loop-closers
The extension does the remembering so the agent doesn’t have to:
- Auto-capture — after each assistant turn, the exchange is stored
at the
AutoCapturetrust tier (importance 0.5, length-gated, 2000-char truncation, silent failure — capture never interrupts a session). Content-hash dedup absorbs repeats. - Wake-up injection — every system prompt gains an
## Agent Memory (ACTIVE)block: a tool reminder, the memory-model cheatsheet (namespaces, what “empty” means), and the home’s wake-up essentials (top-N by importance + recency, refreshed per session). - The bundled
ijimaskill — the deep-dive reference: namespace topology, the diagnostics ladder, and “never conclude wrong-data-dir from agent-side probes.”
memory_save remains the deliberate path: explicit saves land at the
Explicit tier with higher importance — auto-capture is the floor,
not the ceiling.
memory_save parameters
| Parameter | Semantics |
|---|---|
content | The information to remember (required). |
project / topic | Palace room + taxonomy placement (default general). |
importance | 0.0–1.0; higher ranks earlier in wake-up (default 0.8 for manual saves). |
evidence | Observed or Interpreted (default). Observed = directly witnessed (command output, file content, a session event) and requires ≥ 1 citation — the server rejects it otherwise (400). |
citations | Typed pointers grounding an Observed save: { kind: Commit|Report|Session|File|Url, locator }. No citation, no observed claim. |
supersedes | Id of the memory this save corrects. The target drops out of wake-up/search/browse regardless of importance; chain through the successor, never the already-superseded original. |
Wake-up composition is stratified (0.4): the top 12 by
importance-first ordering plus the 8 freshest rows, with superseded
memories excluded from both strata. evidence, citations, and
supersedes are forwarded to POST /memories; absent params leave the
server defaults (Interpreted, no citations, no supersede link) in place.
What the extension provides
| pi tool | Ijima surface |
|---|---|
memory_search (scope=visible: personal + global + staging + member walls) | /memories/search |
memory_save (content-derived id: mem_<hash16>) | /memories |
memory_check_duplicate, memory_delete | /memories/check, /memories/{id} |
knowledge_add/query/timeline/status/invalidate | /kg/* |
Requests are built and parsed in WASM (the ijima-pi crate’s
request/response types), so the wire contract is compiled once and
shared — the pi process itself does no JSON hand-rolling.
Why thin clients
The 0.2.0 topology decision: all workstations are thin clients of one central instance (the “Central Brain” deployment). Local memory state on workstations means fragmentation again — the thing Ijima exists to end. Full local instances with checkpoint sync (satellites) are the 0.3 design, not the 0.2 reality.
Replacing pi-mempalace
If the workstation has an existing pi-mempalace database, import it once:
ijima import mempalace --db <memories.db> --source "$(hostname)"
then point pi at Ijima (env vars or config files), set the home namespace, and retire the local store. The old database remains as a cold backup — nothing in the migration is destructive.
Deploying on a Server
The reference deployment is a single central instance on a trusted
server, with every workstation as a thin client. This chapter condenses
the full runbook (docs/deploy/central-instance.md in the repository) to the
essentials.
Topology
workstations (pi, agents) the central instance (always-on host)
┌────────────────────┐ HTTP ┌──────────────────────────┐
│ IJIMA_URL=... │──────────▶│ ijima serve (systemd) │
│ IJIMA_TOKEN=... │ tailnet │ /var/lib/ijima (NVMe) │
└────────────────────┘ │ zpool/backups (nightly) │
└──────────────────────────┘
- Network: Tailscale; the daemon binds the tailnet interface. TLS via
tailscale serve(terminates on the tailnet’s certs) or thetlsfeature directly. - Storage: data directory on fast local disk (SurrealDB/surrealkv); nightly snapshots to bulk storage.
Provision checklist
- Install the binary (see Getting Started) with
cli,backend-sqlite,embeddings-candle,mining,tlsas needed. /etc/ijima/ijima.toml— host/port/data_dir/issuer_key (copy fromdeploy/ijima.toml.example).deploy/ijima.service→ systemd;systemctl enable --now ijima.- Mint grants per principal (operators, harnesses, machine feeds) — Token Management — via the principals overlay policy.
- Grant shared-namespace memberships for the org walls:
ijima namespace grant ns_ia_shared <principal> --auth <admin>(repeat forns_kellas_shared,ns_shiroyama_shared,ns_writing_sharedas needed). curl .../healthliveness;GET /status(admin) for counts.- Import workstation corpora — Importing Legacy Corpora.
- Point each workstation’s
IJIMA_URL/IJIMA_TOKENat the instance.
Backup & restore
The store is a directory. The drill:
systemctl stop ijima
zfs snapshot zpool/backups/ijima@$(date +%F) # or rsync the directory
systemctl start ijima
Restore = stop, replace the directory, start. Run the drill once before trusting it.
Upgrades
systemctl stop ijima && <install new binary> && systemctl start ijima
Schema definitions are idempotent at open. Check the CHANGELOG for on-disk-layout notes before skipping multiple minors.
Backups and restore drills
SurrealKV is a directory; a naive file copy of a live store can be torn (manifest/WAL disagreeing mid-write) — recoverable, but you want to know before an incident. The pattern that works in production:
- Mirror hourly — rsync the data dir to bulk storage (a snapshot- managed filesystem if you have one). A torn copy is possible; the next hour’s mirror replaces it.
- Drill weekly — copy the mirror aside, boot a throwaway daemon on it (second port, same binary), compare the census against live. Log PASS / DEGRADED (opens, counts differ — writes during the mirror window) / FAIL (does not open). A backup you have never opened is a hope, not a backup.
- Mind permissions — a root-run mirror leaves secrets root-owned; a restore runbook needs the chown step.
Logical export (JSONL through the daemon)
ijima export --url <daemon> --token <admin> [--namespace <ns>] [--out file]
streams every memory (or one wall’s) as JSONL — one {"namespace": …, "memory": {…}} object per line, row count in the x-export-count
header. It runs through the daemon API and so works against a live
instance; the old direct-to-store SQL dump lost the store LOCK race
to the running daemon and is gone (v0.3.0).
The honest tiering: the mirror + drill above remains the primary restore path. Export is the extractable corpus — verification, offline analysis, compliance — and an ad-hoc restore is a documented loop rather than a first-class command:
# restore one wall's rows into another daemon (jq, not a database tool)
ijima export --url old --token t --namespace ns_wall --out wall.jsonl
jq -c '.memory' wall.jsonl | while read -r mem; do
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d "$mem" "$NEW_URL/memories?namespace=ns_wall" > /dev/null
done
Re-POSTing is idempotent at the content layer (duplicates collapse), but provenance fields (source, harness) are re-stamped — a logical restore is a re-ingestion, not a byte copy. Point-in-time fidelity stays with the mirror.
First-day verification
After provisioning: import one real workstation, run a pi session against
the central instance for a day, and confirm /status counts grow. The
deployment isn’t real until a harness has lived on it.
NixOS
Ijima ships a Nix flake: a package (the ijima binary built from the
repo’s own source, with the exact nightly toolchain the release was
verified on) and a NixOS service module.
Quickstart
nix run github:Industrial-Algebra/Ijima -- --help # try the CLI
nix build github:Industrial-Algebra/Ijima # the package
As a flake input
In your NixOS flake:
# flake.nix
inputs.ijima.url = "github:Industrial-Algebra/Ijima"; # or a tag: .../v0.2.1
# host config
{
inputs,
pkgs,
...
}:
{
imports = [ inputs.ijima.nixosModules.ijima ];
services.ijima = {
enable = true;
package = inputs.ijima.packages.x86_64-linux.ijima;
# dataDir = "/var/lib/ijima"; # default; keep on fast storage
# port = 7373; # default
# user = "ijima"; # default: isolated system user
};
}
nixos-rebuild switch and the daemon is up on 0.0.0.0:7373 (adjust
bindAddress). First boot downloads the embedding model (~90 MB) into
dataDir/hf; every boot after is offline.
Options
| Option | Default | Notes |
|---|---|---|
enable | false | |
package | — | packages.x86_64-linux.ijima from this flake |
dataDir | /var/lib/ijima | Store, issuer key, policy overlay, model cache. SurrealKv does random IO — SSD/NVMe. |
bindAddress | 0.0.0.0 | The daemon trusts the network boundary: firewall it, or put a TLS terminator in front |
port | 7373 | |
user | ijima (auto-created) | Set to your own username to let the local CLI share the daemon’s issuer key — token minting and imports without sudo |
memoryMax | 8G | Raise if you run mining |
The unit is hardened ( ProtectSystem=strict, NoNewPrivileges, empty
capability set, … — mirrors deploy/ijima.service) with write access
only to dataDir.
After the first boot
The state directory starts with a fresh issuer key. Mint operator grants from the same host so the CLI shares the daemon’s key:
IJIMA_DIR=/var/lib/ijima sudo -u ijima ijima token issue \
--principal <name> --capabilities memory:read,memory:write \
--policy /var/lib/ijima/policy.toml
(If user is your own account, drop the sudo -u.) Then continue with
the daemon guide and the runbook.
Feature Flags
Ijima is additive-feature-gated: compose the build with the surface you need. The daemon crate’s matrix:
| Feature | Enables |
|---|---|
std | std prelude integration |
http | The axum HTTP daemon |
server-auth | Schubert auth (GrantTokens, policy, verifier) |
backend-surreal | SurrealDB store (embedded kv-mem / surrealkv) — the primary backend |
backend-sqlite | Migration-only SQLite readers for pi-mempalace / ZeroClaw imports |
rate-limit | Schubert intersection-number token buckets |
cli | The ijima binary (clap + reqwest + ijima-client) |
embeddings-candle | Local embeddings (all-MiniLM-L6-v2 via candle/HF) |
mining | Extraction pipeline (ijima-miner + proserpina-agent HTTP LLM tier) |
tls | axum-server/rustls HTTPS (IJIMA_TLS_CERT/IJIMA_TLS_KEY) |
federation | /federation/* control-API scaffold + instance identity |
Defaults: std,http,server-auth,backend-surreal — a production daemon
without the CLI. (The published CLI binary ships cli plus the optional
surfaces.)
Companion crates gate independently: ijima-client’s remote (reqwest
transport) and std; ijima-core’s serde and federation.
Choosing
- Server: default +
cli+embeddings-candle(+mining,tls,backend-sqliteduring migration). - Embedded library use (tests, in-process):
backend-surrealwithouthttp/server-auth— an unauthenticated in-process store. - Thin clients:
ijima-clientalone.
Notes
backend-sqliteexists to read legacy databases once; it is not a runtime store backend.server-authoff means unauthenticated single-process mode — for tests and embedded use only, never a networked daemon.- The pi extension builds from
ijima-pito WASM — seeintegrations/pi.
HTTP API Overview
Ijima’s stable contract is a REST/JSON surface; every route is guarded by
a Schubert capability check and scoped to a namespace. The typed client
is ijima-client. Namespace-sensitive routes accept ?namespace=<ns>
(omit for the caller’s personal namespace; foreign *_private
namespaces are always rejected — except the explicit admin cleanup
paths: DELETE /kg/triples/:id and GET /export accept private walls
as deliberate, logged maintenance targets).
Route map (selected)
| Method | Path | Capability |
|---|---|---|
GET | /health | (none) |
GET | /status | admin |
POST | /memories[?namespace=] | memory:write |
GET | /memories?namespace=&limit= | memory:read (browse) |
GET | /memories/:id?namespace= | memory:read |
DELETE | /memories/:id?namespace= | memory:write |
POST | /memories/check?namespace= | memory:read (dedup pre-check) |
POST | /memories/search | memory:read |
POST | /memories/:id/promote | trust:promote |
GET | /wakeup | memory:read (top-N wake-up context) |
GET | /retrieval/ledger | admin (list events) |
GET | /retrieval/ledger/stats | admin (aggregate counts) |
DELETE | /retrieval/ledger | admin (prune older than before_unix) |
GET | /rooms, /taxonomy, /palace/graph, /tunnel | memory:read |
POST | /kg/triples[?namespace=] | knowledge:write |
POST | /kg/triples/:id/invalidate?namespace= | knowledge:write |
DELETE | /kg/triples/:id?namespace= | admin (hard delete; namespace required) |
GET | /kg/entities/:id, /kg/timeline/:entity | knowledge:read |
POST | /sessions, /sessions/:id/turns, /sessions/:id/end | session:ingest |
POST | /sessions/:id/mine | mining:trigger |
GET | /mining/queue, decisions (accept/reject) | mining:review |
POST | /diaries | memory:write |
GET | /repos/resolve?path= | memory:read (RepoDirectory) |
POST | /tokens/revoke | admin |
GET | /tokens/revocations | admin |
POST | /namespaces/grant, /namespaces/revoke | admin |
GET | /namespaces/members?namespace= | admin |
POST | /doctrine[?namespace=] | admin (global) or doctrine:write (wall-scoped) |
GET | /doctrine/versions[?namespace=&id=] | memory:read (wall) / admin or doctrine:write (global) — list archived revisions |
POST | /doctrine/rollback[?namespace=] | admin — re-activate an archived revision |
GET | /export[?namespace=] | admin (JSONL; x-export-count header) |
GET | /federation/state, routed-write, conflict-signal | federation feature |
The full table (with the store method each route maps to) lives in the
daemon crate’s api module doc comment.
Authentication
One header: Authorization: Bearer <GrantToken>. The grant bundles the
principal’s capabilities; the daemon verifies the signature, checks
revocation, and evaluates each request’s capability geometrically (see
Capabilities).
Conventions
- Errors are JSON with a stable shape;
403means the grant lacks the capability (or the namespace is off-limits),401means the bearer failed verification/revocation,409duplicate content on store. - Provenance fields are accepted on write and echoed on read — the
daemon stamps defaults (
created_at, instance identity) where absent. - All list endpoints are
limit-bounded.
Client
For harnesses, depend on ijima-client (typed async HTTP) rather than
hand-rolling requests — see The Client Crate.
The Client Crate
ijima-client is the typed async HTTP client for harnesses and tools.
It is transport-thin: it builds requests, attaches the bearer, decodes
responses — and nothing else. All policy lives in the daemon.
Setup
[dependencies]
ijima-client = "0.1"
#![allow(unused)]
fn main() {
use ijima_client::{Client, ClientConfig};
use ijima_core::harness::Harness;
let client = Client::new(
ClientConfig::new("http://ijima.tailnet:7373", Harness::Pi)
.with_token(token),
);
}
ClientConfig is cloneable; with_token accepts the raw grant (the
Bearer prefix is added internally).
Surface (selected)
#![allow(unused)]
fn main() {
// memories
client.store_memory(memory).await?; // personal ns
client.store_memory_in("ns_import_laptop", m).await?; // explicit ns
client.recall_memory("mem_x", Some("ns_ia_shared")).await?;
client.delete_memory("mem_x", None).await?;
client.search_memories(&query, Some("ns_ia_shared")).await?;
client.check_duplicate("content", Some(ns)).await?; // dedup pre-check
client.import_memories(ns, memories).await?; // dedup-checked bulk
// knowledge graph
client.add_triple(triple).await?;
client.query_entity("amari", Some(ns)).await?;
client.add_triple_in(ns, &import_triple).await?; // explicit ns
client.invalidate_triple_in(ns, triple_id).await?;
client.import_kg(ns, triples).await?; // bulk import
// sessions & mining
client.ingest_turn(session_id, turn).await?;
client.trigger_mining(session_id).await?;
// palace surfaces
client.list_rooms(Some(ns)).await?;
client.taxonomy(Some(ns)).await?;
client.palace_graph(Some(ns)).await?;
}
import_memories returns ImportCounts { attempted, added, deduped, skipped } — the WS2 import loop in a single call. import_kg returns
KgImportCounts { attempted, added, skipped } and applies each triple’s
valid_to as a post-add invalidation.
All HTTP calls are retried on 429 Too Many Requests with exponential
backoff (250 ms doubling, six attempts, ~16 s total) before the error
surfaces — a rate-limited daemon slows imports down, never drops rows.
Errors
Everything surfaces as IjimaError: Transport (HTTP failure, carrying
status + body detail), Store, or domain errors. A 404 from
recall_memory maps to Ok(None) — absence is not an error.
Feature notes
remote(default) brings reqwest; disable for an in-process stub in tests.- The client identifies its harness in provenance fields — set the real one; the daemon records it on every write.
- For pi specifically, the compiled extension (
integrations/pi) wraps this surface for WASM — pi processes never hand-roll JSON.
CLI Reference
The ijima binary (build with the cli feature) is the operator’s
surface: daemon control, token lifecycle, imports, migration.
ijima serve
Runs the HTTP daemon. Flags override env/config: --host, --port,
--data-dir, --issuer-key. See Running the Daemon.
ijima token
ijima token issue --principal NAME (--capability CAP | --capabilities A,B,C) [--json]
ijima token revoke --token "<bearer>" --url URL --auth "<admin>" [--reason "why"]
ijima token revocations --auth "<admin>" [--url URL]
issueruns offline — it signs with the issuer key in the local data directory, so run it where the daemon’s key lives (or pointIJIMA_DIR/IJIMA_KEYat it).revoke/revocationsare remote calls to a running daemon’s admin routes (--urldefaults to$IJIMA_URL).
See Token Management.
ijima import
ijima import (mempalace|zeroclaw) --db PATH --source NAME
[--namespace NS] [--url URL] [--token TOKEN]
Streams a legacy SQLite corpus into a running daemon with provenance
retagging, per-source namespaces, and dedup pre-checks. Defaults from
$IJIMA_URL / $IJIMA_TOKEN. See
Importing Legacy Corpora.
ijima migrate
ijima migrate [--palace PATH] [--brain PATH] [--embed] [--namespace NS]
The older one-shot local import (writes into the daemon’s own data
directory, no HTTP). Prefer import.
ijima namespace
ijima namespace grant <NS> <PRINCIPAL> --url URL --auth "<admin>"
ijima namespace revoke <NS> <PRINCIPAL> --url URL --auth "<admin>"
ijima namespace members <NS> --url URL --auth "<admin>"
Shared-namespace membership management (WS3 org walls) on a running
daemon — admin capability required. Grants are upserts (idempotent),
revokes are idempotent, members list oldest-grant-first with the
audit trail (granted_by, granted_at_unix).
ijima doctrine ingest
Flat seed-pack mode (frontmatter + body files):
ijima doctrine ingest --dir doctrine/ --url http://127.0.0.1:7373 --token <admin>
Tree mode (v0.3.0) — walk a markdown corpus into a wall:
ijima doctrine ingest --root ../IA-documents \
--include 'RESEARCH_REPORTS/*' --exclude 'ARXIV*' \
--namespace ns_ia_doctrine --url ... --token <admin-or-doctrine:write> \
--dry-run
Stable doct_<hash12(relpath)> ids make re-runs idempotent (edits
upsert, additions append); id-carrying frontmatter passes through
verbatim; byte-identical files collapse to one row. --dry-run
prints the plan without a daemon (--url/--token optional there).
Since 0.4.0, each ingest response carries the doc’s revision (stable
body identity — see provenance) and any
advisory warnings from the stance scan (density, accretion,
long-dense patterns) for the corpus reviewer. Changed bodies archive
the outgoing version server-side; POST /doctrine/rollback {"id", "to"}
(admin) restores any archived version.
ijima kg-delete
Hard-deletes one triple (the misplaced-data cleanup tool — soft
retirement is invalidate):
ijima kg-delete --id 'X:relates_to:Y' --namespace ns_wall --url ... --token <admin>
Admin-only; --namespace is required (destructive ops never default
silently); private walls are valid targets.
ijima export
ijima export --url http://127.0.0.1:7373 --token <admin> [--namespace ns] [--out file]
Logical JSONL export through the daemon (v0.3.0): one
{"namespace": …, "memory": {…}} object per line, row count in the
x-export-count header, stdout by default. Works against a live
daemon — the old direct-to-store SQL dump (which lost the store LOCK
race) is gone. See the deploy guide for the backup tiering and the
re-ingest restore loop.
Exit codes and errors
Errors print ijima: <message> on stderr and exit non-zero. Remote
commands surface HTTP detail verbatim — a 403 from the daemon names
the missing capability.
Security Considerations
Ijima’s access model is Schubert capability algebra on the Grassmannian Gr(4,8) — quantitative, geometry-based authorization, not ad-hoc role checks.
Identity & authentication
- Principals are operators or harnesses; a request is always
(principal, harness, action). - GrantTokens are proof-carrying, ed25519-signed bearer credentials
bundling one or more capabilities; the capability list is inside the
signed blob. Verified by the daemon’s
GrantVerifier. - Write implies read in the geometry (
memory:write⊇memory:read); the converse never holds. - Revocation: store-backed SHA-256 hash list checked after signature verification; survives daemon restarts; raw bearer values never touch the store, logs, or backups. Issuer-key rotation remains the emergency lever (invalidates every grant at once).
Authorization
Eleven capabilities as Schubert partitions; the intersection number
(codimension) of a capability is both its authorization weight and its
rate-limit capacity. memory:read (codim 1) → 1× throughput;
memory:write (2) → 2×; admin (the point class σ₄₄₄₄, codim 16) → 16×.
The geometry of access maps to the geometry of throughput.
Trust tiers & trust-flow
Trust-tier transitions are themselves capabilities (the
provenance-tier model): trust:promote gates tier promotion — raising
trust is costlier than writing at a tier. trust:endorse and
trust:override (default-deny) cover cross-tier endorsement and
authority override. The same geometric policy therefore governs who may
access and how trust may flow. Imported content lands AutoCapture
regardless of origin claims — an import is a claim, not a credential.
Boundaries
- Namespace isolation is enforced at two layers:
resolve_nsat the API (foreign*_privateis always forbidden) and namespaced record keys at the store (same logical id in two namespaces cannot collide or cross-read). - Promotion is the single redaction boundary — the one place content filtering (secret stripping) happens. Personal storage is always verbatim.
- Provenance (origin instance + authority scope + source tier) on every memory is the foundation for federation cross-talk policies and context-poisoning protection.
Known limitations (honest)
- v0.2.0 is single-instance: federation routes exist as a scaffold; cross-instance enforcement is 0.3+.
- Rate limiting is per-principal token-bucket; it is not a DoS shield — deploy behind a real proxy on untrusted networks.
- TLS is opt-in (
tlsfeature); on a private Tailscale network, plain HTTP behindtailscale serveis the documented default. - GrantToken expiry is adopted (Schubert 0.5); the instance-side revocation list remains as defense-in-depth, and CRDT tombstones are deferred to 0.3 satellites.
- Context-poisoning protection is designed but not yet implemented.
See docs/DESIGN.md and docs/adr/ (grant-token-migration,
token-revocation, provenance-tier-model).
Architecture
The workspace
ijima-core domain types + Store/KnowledgeGraph traits + capability vocab
ijima-server axum daemon, SurrealDB backend, Schubert auth, CLI
ijima-client typed async HTTP client (thin clients)
ijima-miner extraction engine (rules + LLM tiers)
ijima-pi pi extension types (compiled to WASM for integrations/pi)
Dependencies flow one direction: server → core/miner/client; miner →
core (+ proserpina-agent for the LLM tier); client → core. ijima-core
has no HTTP, no database, no async runtime beyond trait definitions —
the domain is pure.
Store backends
- SurrealDB (
backend-surreal) — the primary backend. Embedded engines:kv-mem(tests, ephemeral) andsurrealkv(persistent single-file). Record keys are namespace-composite (<ns>:<id>); tables are defined idempotently at open; every filtered column has aDEFINE INDEX(SurrealDB does not auto-index). - SQLite (
backend-sqlite) — migration-only readers for the legacy pi-mempalace / ZeroClaw corpora. Never a runtime backend.
The daemon
serve builds the router once at boot: middleware order is
authentication (bearer → GrantToken verify → revocation check) →
rate limiting (intersection-number token buckets) → capability check
per-route → resolve_ns → handler. All handlers are thin: they map
HTTP onto Store/KnowledgeGraph trait calls. The mining feature adds
the extraction pipeline as an in-process stage over the session
repository.
Deployment modes
- Daemon + thin clients (the 0.2.0 “Central Brain” topology): one
instance on an always-on host; workstations point
IJIMA_URLat it. - Embedded in-process: build
backend-surrealwithouthttp/server-authfor tests and embedding — an unauthenticated direct store. - Satellites (0.3 design): full local instances with checkpoint sync to the center; the federation control API scaffold is the seed.
Provenance by construction
Provenance is not a bolt-on: Memory is content + provenance in the
domain type. Every write path (HTTP, import, mining, doctrine ingest)
must produce a full provenance block; every read path carries it back.
This is what makes trust tiers and (future) federation conflict
resolution enforceable at the type level.
Decision Log (ADRs)
Architecture decisions live as ADRs in docs/adr/ in the repository.
Index with one-line outcomes:
| ADR | Decision |
|---|---|
grant-token-migration.md | Adopt Schubert 0.4 GrantTokens (multi-capability, partition-signed) as the sole bearer format; delete the duplicated wire codec; admin via geometry (point class), not string equality |
token-revocation.md | Store-backed SHA-256 bearer-hash revocation list (no raw bearers at rest); checked after signature verification; expires are upstream (Schubert 0.5) — revocation and expiry are complementary |
namespace-membership.md | Shared namespaces are membership-gated in the store (not policy TOML) — runtime-mutable, auditable, effective against already-issued grants; promotion targets gated too |
schubert-0.5-adoption.md | Full 0.5 adoption in the zero-re-mint window: signed expiry (--expires-in), policy-constrained issuance (principals-only overlay files, fails closed), instance-side revocation kept as defense-in-depth while CRDT tombstones wait for 0.3 satellites |
provenance-tier-model.md | MemorySource trust grades map to Schubert codimensions; trust transitions are capabilities; imports land AutoCapture |
miner-architecture.md | Two-tier extraction (deterministic rules + optional LLM via proserpina-agent); confidence-routed to auto-file or review queue |
compaction-recovery.md | Session compaction keeps recoverable turn history for re-mining |
federation-control-api.md | /federation/* control scaffold with instance identity (IJIMA_INSTANCE_*); boundary enforcement staged for 0.3 |
Standing decisions recorded elsewhere
- Thin clients in 0.2.0, satellites in 0.3 — all workstations point
at the central instance; local instances with checkpoint sync are the
next design (
docs/plans/). - Membership-in-store over policy-TOML grants for shared namespaces — mutable at runtime without redeploying.
- Config precedence defaults < file < env < CLI, and an explicit
$IJIMA_CONFIGpointing at a missing/malformed file is a hard error. - Announcements stay manual — the reusable auto-announce workflow will be proven on another project first.
Roadmap
Shipped:
- v0.4.0 “The Trust Machinery” (2026-10-09) — evidence grades
(
Observed/Interpreted+ typed citations; observed claims cite or they don’t ship), supersede links (corrections displace their targets — the correction ritual became a mechanism), doctrine versioning with stable revision identities + one-command admin rollback + advisory stance-validation warnings, stratified wake-up (8-freshest + 20-fill; the starvation fix), live-only dedup preflight, surrealdb 3.3.0 (rkyv 0.8 in the build graph). - v0.3.1 (2026-09-21) — declarative per-project namespaces
(
.pi/ijima.json),IJIMA_HOME, the token-fallback~expansion fix. - v0.3.0 “The Curated Brain” (2026-09-10) — org-scoped doctrine
walls,
doctrine:writeleast-privilege capability, tree-mode ingest CLI, AutoCapture TTL sweeper, KG hard-delete, logical export, npm trusted publishing for the pi extension. - v0.1.0 (2026-08-10) — the library: two-store model, Schubert capability auth, SurrealDB backend, mining pipeline, pi extension, crates.io publication of core/server/miner/client.
- v0.2.0 “Central Brain” (2026-08-21) — the deployment release: GrantToken migration + signed expiry + policy-constrained issuance (Schubert 0.4 → 0.5), token revocation, config-file layer + deploy kit, multi-source import over HTTP (WS2), membership-gated org walls (WS3), Proserpina agent surface (WS0), surrealdb 3, the book you are reading.
- v0.2.5 “Agent Homes” (2026-08-29) — shared home namespaces for
agents (
IJIMA_NAMESPACE+ namespaced wake-up): fleet knowledge lives in the org wall, not per-host silos; org walls keep other orgs out. Plus URL file fallback and content-derived memory ids. - v0.2.3 “Loop-Closers” (2026-08-23) — the pi extension completes
the memory loop: auto-capture per turn, wake-up injection per session,
token-file fallback; the bundled agent skill;
repo_directoryDDL fix (fresh-store/reposwas a 500). - v0.2.2 “Visible World” (2026-08-22) — the “empty brain” fix:
scope=visiblesearch now spans the principal’s readable world (private + global + import staging + member org walls); first-party NixOS flake (package + service module);HashEmbeddertest utility. - v0.2.1 “Field Lessons” (2026-08-21) — first production deployment hardening: knowledge-graph import (entities + triples alongside memories, id-is-name re-addressing), client 429 backoff (rate-limited daemons slow imports down, never drop rows), reproducible pi-extension build. Found on the fleet’s first production deployment.
Shipped (0.3.0 — “The Curated Brain”, 2026-09-10):
- Markdown corpora into walls — namespace-scoped doctrine ingest
(
?namespace=), thedoctrine:writecapability for unattended timers, tree-mode CLI (--root, stable path-derived ids, dry-run), and duplicate-collapse semantics for idempotent re-runs. The first corpus: the research-report corpus, two walls (reports+docs / arxiv), a daily morning ingest ahead of the day’s sessions. - AutoCapture TTL sweeper — tier-gated lifecycle: ambient chatter ages out (30d default), deliberate saves and doctrine never do.
- KG hard-delete + logical JSONL export through the daemon — operator hygiene that works against a live instance.
- npm publishing via trusted publishing (OIDC) in CI.
Next (0.4 horizon):
- Satellite sync — full local instances with checkpoint export/push to the center (the WS6 design seed); the federation control API grows into enforcement.
- Batch ingest (
turns:batch) for machine feeds (Minoru mining, Quantizon experiments) and scheduled mining (CLI + systemd timer). - Contemplative loop plumbing — the daily reflection organ reads wake-up + deltas + a randomness seed and writes back into the corpus (IA-research hosts the prompt; Ijima already carries the loop).
- Evidence grading & context-poisoning protection — elevated from the
long game (2026-09-13): the report apparatus now writes agent-authored
memory daily (learned facts, archaeology gap-closure), so
speculation-as-fact is a structural exposure, not a hypothetical.
Candidate direction D: evidence-class markers on saves
(
observedvsinterpreted), citation requirements, retrieval-side weighting. Prompt-layer gate already shipped in IA-research as containment; the enforced mechanism lands here. - Block↔memory promotion boundary — the doctrine note (Lonis Block kinds × promotability × trust tiers) that Wallace and Ijima will implement against.
Long game:
- Networked instances / federation cross-talk policies.
- The doctrine-authority question: is Ijima the authoritative doctrine store, or an opt-in doctrine-health contract?
The authoritative, continuously-updated version lives in the repository:
docs/ROADMAP.md.
Basic Usage
A round-trip: store, search, mine, promote. Assumes a running daemon
(ijima serve) and a grant issued via ijima token issue.
Store and recall
TOKEN="..."
curl -s -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{"id":"mem_1","content":"Amari is the flagship math library",
"project":"amari","topic":"project-context",
"source":"Explicit","harness":"Pi",
"importance":0.7,"created_at":"0"}' \
localhost:7373/memories
curl -s -H "Authorization: Bearer $TOKEN" localhost:7373/memories/mem_1
Dedup pre-check
curl -s -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{"content":"Amari is the flagship math library"}' \
localhost:7373/memories/check
# {"duplicate":"mem_1"}
From Rust
#![allow(unused)]
fn main() {
let client = Client::new(
ClientConfig::new("http://127.0.0.1:7373", Harness::Pi).with_token(token),
);
client.store_memory(memory).await?;
let hits = client
.search_memories(&SearchQuery { text: "flagship".into(), ..Default::default() }, None)
.await?;
}
Mine a session
# ingest a turn
curl -s -H "Authorization: Bearer $INGEST_TOKEN" -H "content-type: application/json" \
-d '{"turn_index":0,"role":"User","content":"we decided to use surrealdb","timestamp":"..."}' \
localhost:7373/sessions/sess_1/turns
# trigger mining (mining:trigger grant)
curl -s -X POST -H "Authorization: Bearer $MINER_TOKEN" \
localhost:7373/sessions/sess_1/mine
# review the queue (mining:review grant)
curl -s -H "Authorization: Bearer $REVIEW_TOKEN" localhost:7373/mining/queue
Accepting a queued extraction files it as a Mined memory with the
source session in its provenance; rejecting archives it.
Promote
An imported or mined memory earns trust explicitly:
curl -s -X POST -H "Authorization: Bearer $TRUST_TOKEN" \
localhost:7373/memories/mem_imported_1/promote
Full walkthroughs
- Multi-Source Import — consolidating workstation corpora.
- Guides: Import, Mining, Deploying.
Multi-Source Import
Consolidating two workstations’ pi-mempalace corpora into one central daemon — the WS2 workflow end to end.
Scenario
elliotthall-laptopandkaiizeneach have a pi-mempalacememories.db; the same insight was sometimes saved on both.- The central daemon runs at
ijima.tailnet:7373.
1. Mint an import grant (once)
ijima token issue --principal importer \
--capabilities memory:read,memory:write --json
export IJIMA_URL="http://ijima.tailnet:7373"
export IJIMA_TOKEN="<grant>"
2. Import each source
On each workstation (scp the db to the server, or run locally with
IJIMA_URL pointed over the tailnet):
ijima import mempalace --db ~/.pi/agent/mempalace/memories.db \
--source "elliotthall-laptop"
# ijima: import `elliotthall-laptop` complete — 1190 added, 91 deduped, 3 skipped
ijima import mempalace --db /srv/kaiizen/memories.db --source "kaiizen"
# ijima: import `kaiizen` complete — 940 added, 150 deduped, 0 skipped
Each lands in its own staging namespace — ns_import_elliotthall_laptop
and ns_import_kaiizen — with origin stamped per source and every
memory at the AutoCapture tier, even rows the source called
manual-save.
The deduped counts are per-source content-hash collisions (the same
memory saved twice on one machine). Cross-source overlap is preserved
deliberately: both namespaces keep their copy, each tagged with its
origin, until you review.
3. Inspect per-source overlap
curl -s -H "Authorization: Bearer $TOKEN" \
"localhost:7373/memories?namespace=ns_import_kaiizen&limit=50" \
| jq '.[] | select(.origin=="kaiizen") | .content'
4. Promote what you trust
Review in the staging namespaces, then promote winners into personal or
shared namespaces — trust:promote grant required:
curl -s -X POST -H "Authorization: Bearer $TRUST_TOKEN" \
"localhost:7373/memories/mem_9812/promote?namespace=ns_import_kaiizen"
The origin stamp travels with the promoted memory — the workstation trail survives promotion.
5. Re-run safety
Imports are idempotent: re-running the same command reports the previous
added count as deduped and adds nothing. Schedule freely.