MoriMori
Mori

How Mori works

The full path of a message — routing, the tool loop, local dispatch, and on-disk verification — running on your machine with your keys.

The big picture

Mori is an Electron app split into two halves. The renderer holds the brain: chat.ts owns every turn — routing, provider calls, the tool loop, memory retrieval, and post-turn verification. The main process holds the hands: a single dispatcher, executeActionCore, is the only code that touches your machine — files, shell, apps, documents, email, Discord. Every tool the model calls crosses exactly one IPC boundary (execute-action) into that dispatcher, which means every side effect passes through one auditable choke point.

Mori is local-first and bring-your-own-keys. Model requests go directly from the app to the provider you configured — Anthropic, OpenAI, OpenRouter, or any OpenAI-compatible provider (Groq, DeepSeek, xAI, Together, Mistral, Perplexity). Provider resolution is strict: if your selected provider has no key, Mori falls back to another provider you have entered a key for, and if none exists it tells you to add one. It never silently reroutes agent work elsewhere.

Routing: the first decision

A composer slash layer runs before anything else. Text that parses as a known command — /usage, /model, /compress, /clear, /autonomy, and the rest of the local registry — executes on the spot and never reaches a provider; only a leading / followed by a recognized name is intercepted, so ordinary prose starting with a slash still routes normally. Everything that is not a command becomes a turn, and the turn is classified by intent and sent down one of four paths.

  • Browser tasks (shopping, webmail, "go to this site and do X") take a DOM-driven browser agent. If the Mori Chrome extension is connected, the run happens inside your real Chrome — your logins, your sessions, no extra key needed. Otherwise a fast DOM agent reads elements and clicks by selector, driven by Claude via your Anthropic key. No screenshots in the hot loop.
  • Explicit computer control ("use the computer", "click around") takes the vision computer-use loop. Screenshots stream to the UI so you watch what Mori sees; autonomy defaults to ask, so each risky step raises a confirmation.
  • Clearly actionable tasks go to the agent runtime: classify, plan, execute, verify. The runtime never returns fake success — if verification fails, the reply says so. Low-confidence tasks fall through rather than guessing.
  • Everything else takes the LLM path. Pure conversation streams token-by-token on a cheaper fast model (smart routing; disable with smartRouting: false). Anything that might need a tool — a memory request, a follow-up to a recent build — is forced onto the tool loop with your chosen strong model, because the streaming path carries no tools and must never pretend to act.

What the model actually sees

The prompt is built for byte-stable caching. The system prompt carries only session-stable content — Mori's core instructions, your host OS, a memory snapshot frozen at session start, and your persona settings — and is marked with a cache breakpoint, so repeated turns hit cached input at roughly a tenth of the cost. Everything that changes per turn rides in a <turn_context> block appended to a copy of the latest user message, never persisted into saved history: the current date and time, memories relevant to this message (scored by token rarity plus recency), playbooks from similar past tasks, failure lessons, active goals, and any task left unfinished by a previous session.

Conversation history uses a verbatim window of the last 20 messages. Older turns do not vanish: a background pass maintains a running summary (under 200 words, persisted per conversation) that is prepended to the turn context, so long sessions keep their goals, decisions, and file paths. The refresh runs after your reply lands, never in parallel with the turn's own request.

The tool loop

Mori runs two equivalent tool loops — one speaking the Anthropic Messages format, one speaking OpenAI-style chat completions — so every provider gets the same behavior. Each step, the model returns tool calls; Mori executes them, appends the results, and asks the model to continue, until it answers in plain text.

  • Step budgets come from the active thinking stage: Spark caps at 15 tool steps, Flow at 50, Forge at 90, Trance at 200. Prefix a single message with a stage name to override one turn without changing the session.
  • Read-only tools (reads, searches, HTTP, OCR) and up to three run_subtask subagents dispatch in parallel; anything stateful — terminal commands, file writes, the browser — stays strictly sequential.
  • Truncated or invalid tool-call JSON never executes. The model gets an error telling it to re-issue the call in smaller pieces, instead of a half-written file silently "succeeding".
  • Messages you send mid-task fold into the very next loop step as steering notes — no competing second turn. Pause and Stop are honored between every tool call, and Stop also kills any shell children the agent spawned.
  • Every provider call carries a 150-second timeout and one retry on 429/5xx, so a hung provider cannot freeze a task.
  • The whole loop is session-scoped. Steering notes, Pause/Stop, the usage meter, and post-turn verification all key off a session id, so each Command Deck pane runs its own independent loop — stopping one pane never touches another, and two panes can be mid-task at the same time.
> forge: rebuild the pricing page and verify every link
[Chat] Per-turn stage override → Forge
[Chat] Provider: Anthropic | Model: claude-sonnet-4-6
[Chat/Anthropic] Tool calls (step 1): read_file, list_files
[Chat/Anthropic] 2 read-only tools dispatched in parallel
[Chat/Anthropic] Tool calls (step 2): edit_file

Dispatch and the safety gate

Every tool call lands in executeActionCore in the main process. Before anything runs, arguments are normalized — common alias names are mapped onto canonical keys, ~ expands to your home directory, and bare relative paths resolve against the Desktop — then the call passes through the safety gate.

  • Hard blocks, no override: fork bombs, filesystem formats, raw disk writes, and recursive deletes of protected roots (/, ~, /System, /Users, ...) are refused outright. If you truly intend one, Mori tells you to run it yourself.
  • Approval-gated: destructive-but-legitimate commands — sudo, recursive or forced rm, git reset --hard, git clean -f, killall — raise a native confirmation dialog, and deleting any file always asks first. This is on by default.
  • Purchases stop at the pay button. Browser checkout runs hold the order at the final step and surface a confirm gate in the UI; the order is only placed after you explicitly approve. Full Auto mode removes clarifying questions, not these gates — sending, purchasing, deleting, and publishing still require confirmation.

Past the gate, the dispatcher routes to platform-native handlers and the action switch: file operations, real deliverables (DOCX, PDF, XLSX, PPTX, zip archives), send_email with smart attachment resolution, web search, Discord and Telegram delivery, and app control. Everything executes locally and returns a plain-text result the model reads on its next step.

Context hygiene: the sanitizer

Every message array passes through a sanitizer immediately before every provider request — the last line of defense against a malformed transcript turning into an API 400 that eats the whole turn. On a healthy conversation it is a no-op. When something is off — a stopped turn left a tool call unanswered, an empty block slipped in, a steering note landed between a call and its result — it repairs the array in place and logs each repair.

  • Anthropic format: no empty messages, the first message is a user message, roles strictly alternate, every tool_use id is answered by a tool_result in the next user message (missing ones are synthesized as cancellation errors), and orphan results are stripped.
  • OpenAI format: every tool_calls entry is answered by a role: tool message before any other message, orphan tool messages are dropped, and null content is normalized.

Proof, not claims

The model's final text is not treated as ground truth. After the reply is produced, Mori re-checks this turn's successful mutations against reality: created files must actually exist on disk, created folders must list, deleted files must be gone, and a started dev server must answer a network probe. Up to 12 checks run with 3-second timeouts, and a verdict line is appended to the reply — or a warning naming exactly what could not be confirmed.

A separate honesty check polices send claims on the tool-loop route: if the reply says something was sent or emailed but no send-capable tool succeeded this turn, Mori appends an explicit correction rather than letting a fabricated "done" stand. Replies produced without any tool calls are screened the same way — success claims like "your tweet has been posted" are blocked and replaced with an honest answer.

Once the reply returns, background jobs finish the turn without blocking you: the task ledger closes (a completed task saves a reusable playbook, a failed one records a lesson), the exchange is mined for durable facts worth remembering, active goals are judged for progress, and the context summary refreshes for next time.

✓ Verified: 3/3 changes confirmed on disk.

⚠️ Verification: 1/2 confirmed — could not verify: index.html.