The full path of a message — routing, the tool loop, local dispatch, and on-disk verification — running on your machine with your keys.
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.
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.
ask, so each risky step raises a confirmation.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.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.
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.
run_subtask subagents dispatch in parallel; anything stateful — terminal commands, file writes, the browser — stays strictly sequential.> 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_fileEvery 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.
/, ~, /System, /Users, ...) are refused outright. If you truly intend one, Mori tells you to run it yourself.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.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.
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.
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.tool_calls entry is answered by a role: tool message before any other message, orphan tool messages are dropped, and null content is normalized.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.