Integration
How the seat meets the rest of the world: the memory systems it learns from, the concurrency designs it voted on, the runners it sits in, and the dogfooding that tests it every day. The explanation says what the seat is; this page says what it plugs into and why.
The seat among FOSS memory systems#
Agent memory systems in the literature and on the market do one of a few jobs. The seat integrates with them rather than re-running them:
System |
What it does |
How the seat meets it |
|---|---|---|
Mem0 (doi:10.48550/arXiv.2504.19413) |
a model extracts facts from a conversation and decides per fact whether to add, update, delete or leave the store |
the seat’s proposals path does the same behind a check: a proposal becomes a claim only on an explicit accept. At |
Zep (doi:10.48550/arXiv.2501.13956) |
a temporal knowledge graph with edges carrying the time an edge became true and stopped being true, plus fused lexical, dense and graph retrieval |
the pack carries the same two times on every claim (written-at and the live window a superseding claim closes) and fuses a prefix scan, BM25+ and a dense ballot; a Zep graph can be read as trust rows and island links, not as a second store |
Letta, formerly MemGPT (doi:10.48550/arXiv.2310.08560), with sleep-time compute (doi:10.48550/arXiv.2504.13171) |
a small core memory the model edits with tools, plus recall and archival stores reorganised between turns |
the seat’s review clock ( |
HippoRAG (doi:10.48550/arXiv.2405.14831, doi:10.48550/arXiv.2502.14802) |
a knowledge graph retrieved by personalised PageRank from the entities a question names |
the island ( |
MemoryBank (doi:10.48550/arXiv.2305.10250), Generative Agents (doi:10.48550/arXiv.2304.03442) |
forgetting on an Ebbinghaus curve refreshed by recall; ranking by recency, importance and relevance plus reflection |
forgetting here is by review, not by age: FSRS retrievability scales search, and a never-recalled lesson lapses like a missed review |
Seldon (seldon-code/seldon) |
the Ordinary Differential Equation (ODE) engine for opinion dynamics |
|
The one design choice the others do not make is the privacy of write:
what is remembered is what was said with Remember or Prefer, stored
as written. Integration never crosses that line: foreign facts arrive as
proposals, trust rows, or cited deeds, each attributable and retractable.
Three concurrency designs and the vote#
Three designs were on the table for the seat’s concurrent life: many agents writing at once. The vote went as follows, and the code follows it.
A Go rewrite for goroutines: no. The bottleneck is not compute, it is
one writer. The pack is one daemon owning one Lightning Memory-Mapped Database (LMDB) file
(packsetd); the claim graph is one in-process graph with the host as
sole mutator (claimdag); deed appends take a file lock so two processes
minting at once write whole lines. The packset README records zero
errors at 32 clients, and a herd of 32 writing 6400 distinct claims into
one workspace keeps 6382 live and 18 closed. Goroutines parallelise what is already fast
and leave the single writer single; a rewrite would trade the Rust
type-level guarantees (closed enums for kind, status and role; compare-and-swap on
generation) for a runtime that does not change the contention shape.
Concurrency here is checked with a hammer test: packsetd serves many
readers and one writer, and the hammer rerun is the regression check.
An Erlang-style plugin tree: no as an Open Telecom Platform (OTP) port, yes as the pattern.
What Erlang would buy is supervision: a crashed worker restarts, and a bad
message is refused at the boundary. The seat
already has that shape without the runtime. ljos-policyd (or
phronesis_check_shell behind it) refuses at the boundary. A policyd that is
missing or slow does not stop the work: the command runs and ljos logs a warning.
ljos doctor names each habitat and whether it answers,
and a missing one is reported rather than guessed around. The claim graph
lives in $XDG_RUNTIME_DIR so a reboot clears leases instead of
resurrecting stale locks. A new plugin therefore looks like a habitat:
one store, one CLI, one Model Context Protocol (MCP) surface, doctor-visible. That is
cheaper than an OTP tree and keeps every store a file a person can read.
herdr as the tooling integration: yes, and it is the ongoing work.
herdr is the session multiplexer the seat already speaks: personas
reason in a herdr pane when herdr is up and fall back to a tmux window
when it is not (crates/ljos-cli/src/persona_session.rs), the runner
works in the persona’s home as its own seat, and the person’s watch-and-talk
window is that same pane. The direction is more of this same
system: herdr panes as supervised seats, policyd refusing writes
to the seat’s own files, approvals flowing through ljos approve on any
runner. The Grok Build page is the reference
integration; each runner registers the events its table names.
Deep runner integration#
ljos onboard --harness NAME registers the MCP server, the hooks and the
skill for one runner. NAME takes claude, codex, grok, cursor,
antigravity, opencode, omp, hermes, copilot, gemini,
windsurf, factory, kiro, cline, qwen, crush, zed, vscode,
claude-desktop, amazonq, aider (conventions file, advisory), auggie, amp, goose, openhands,
continue, roo, kilo, grokbot or shell. The contract is the
same everywhere and the envelope differs:
Runner |
Memory injection |
Shell check |
Ask |
Notes |
|---|---|---|---|---|
Claude Code |
|
|
native permission prompt |
plugin |
Codex |
hooks plus MCP server list |
hook deny blocks |
none: Codex does not prompt; an ask is rewritten to a deny that offers |
prompt and |
Grok Build |
|
|
|
frozen file |
Antigravity ( |
hooks plus skill |
hook deny blocks |
|
pipeline stages arrive separated by a |
opencode |
|
hook deny blocks |
runner prompt or |
same contract, TypeScript hook |
omp |
|
hook deny blocks |
runner prompt or |
same contract, TypeScript hook |
hermes |
MCP server list plus skill |
advisory: onboard registers no hook |
none: no hook |
nothing leaves the machine unless a remote judge is turned on |
Cursor |
prompt note on the first tool result; skill; |
|
Cursor’s permission prompt on a shell command |
the IDE and the |
GitHub Copilot CLI |
skill; no prompt note, since Copilot drops a command hook’s prompt output |
|
Copilot’s permission prompt |
the same file in a repository’s |
Gemini CLI |
|
|
none: an ask is a deny that says so |
hooks and MCP server share |
Windsurf (Devin Desktop) |
skill |
|
none |
|
Factory droid |
|
|
droid’s permission prompt |
|
Kiro |
skill |
|
none |
|
Cline |
|
|
none |
two scripts in |
Qwen Code |
|
|
Qwen’s confirmation |
|
Crush |
skill |
|
none |
|
Auggie |
skill |
|
none: an ask is a deny that says so |
|
Amp |
skill |
|
Amp’s prompt, from exit 1 |
|
goose |
skill in the plugin |
|
none |
user plugin |
OpenHands |
|
|
none |
|
Continue |
rules file; |
advisory: hooks in |
runner prompt |
|
Roo Code, Kilo Code |
skill |
advisory: no tool hook |
runner prompt |
the extension’s |
Grok Bot |
the skill, then |
|
the person runs the command |
no MCP server and no hook file; source the env file |
Codex runs a hook only after you trust it. After ljos onboard --harness codex,
open /hooks and trust each ljos hook before it runs.
Trust it again after an upgrade rewrites one. Until then the hook does not run,
and policyd sees none of Codex’s commands. ljos doctor reports an untrusted hook when
it can see one.
The runners from Copilot down call ljos hook --runner NAME. That
command reads the runner’s payload as Claude Code’s, runs the same hook,
and writes the answer back in the runner’s field names and exit code.
On a tool call, the relay lets the command run and logs a warning in three cases.
The inner hook crashes, exits non-zero without an answer, or runs past GUARD_TIMEOUT.
That is 5 seconds unless LJOS_GUARD_TIMEOUT names another number of seconds.
A policyd that is missing, hangs, crashes, or prints no answer does not
stop the command either. POLICYD_REQUIRED=1 refuses the line when no binary answers.
On any other event, such as a prompt, the relay waits 9 seconds, past
the inner hook’s own 8 second deadline, and a fault lets the event
through with a note.
When a hook fails#
A check works only when its hook runs. What happens when the hook breaks is the runner’s call, and the runners disagree. The table says what happens to the command when the seat’s hook crashes, times out, or prints something the runner cannot read. “Runs” means the command goes ahead unchecked.
Runner |
Hook exits non-zero (not 2) or crashes |
Hook times out |
Answer is not JSON |
Source |
|---|---|---|---|---|
Claude Code |
runs; only exit 2 blocks |
runs |
runs |
|
Codex |
runs; only exit 2 with a reason on stderr blocks |
runs |
runs |
|
Cursor |
runs: the seat does not set |
runs |
|
Cursor hooks docs |
GitHub Copilot CLI |
blocked |
runs |
falls through to Copilot’s own permission flow |
|
Gemini CLI |
runs, with a warning |
not documented |
runs |
|
Windsurf (Devin Desktop) |
runs; only exit 2 blocks |
not documented |
not read; the exit code decides |
|
Factory droid |
runs; only exit 2 blocks |
not documented |
not read; the exit code decides |
|
Kiro |
blocked |
not documented |
not read; the exit code decides |
|
Cline |
not documented |
not documented |
not documented |
|
Qwen Code |
runs; only exit 2 blocks |
not documented |
not read; the exit code decides |
|
Crush |
runs; only exit 2 blocks |
runs |
not read; the exit code decides |
The seat does not turn a fault into a refusal. A ljos-policyd that is missing, hangs, crashes,
or prints no answer lets the command run, and ljos logs a warning. So do a relay that cannot read its
input and a wait past LJOS_GUARD_TIMEOUT. CI and a fresh install are never locked out.
The hook files the seat writes give each runner a
timeout of 10 to 20 seconds. A check that takes longer meets the
“times out” column. None of this helps when the ljos binary itself
is gone. On Copilot and Kiro that blocks every shell command, and on
every other runner the commands run unchecked.
When the seat’s own hook fails on a permission event, it writes an allow in the runner’s JSON, so a runner that blocks on a missing answer still runs the command.
Two rules hold on every runner. First, policyd refuses a command
that writes the seat’s own files: its binaries, its hook entries and its policy files.
An agent cannot rewrite the law it runs under. ljos adds one more refusal, for a command or
a file tool that writes the tracker’s files by hand.
Second, ljos policy -- COMMAND prints the answer for one command without running it,
so every integration is testable without the runner. scripts/smoke.sh does exactly that.
policyd can also answer a command with a fixed one. This is coming in 0.35.0. The new line runs only
when the guard also allows it. Claude Code, Codex, Grok Build and Cursor’s preToolUse take it
through the hook’s updated input. Every other runner in the table gets a refusal that names the
new command, so the old line never runs.
Dogfooding#
This repository eats its own cooking. Sittings are claimed through the
claim graph, lessons are remembered with ljos remember and reviewed
with ljos due, panel decisions are settled with ljos consensus and
calibrated with ljos calibrate, and releases cite their deeds. When an
integration breaks, the breakage is filed as an issue in the tracker
first, because a conversation that has no issue is told to file one
and sit. The measure of this page is whether the next runner takes an
afternoon: one envelope, the events that runner names, one contract.