# akari user guide (full) > The complete user guide, concatenated in reading order. Canonical pages live under https://akari.jessica.black/guide. # akari user guide > **Reading this as an agent?** The whole guide is also served as a single > plain-text file at [`/llms-full.txt`](/llms-full.txt), so you can ingest every > chapter in one fetch instead of crawling pages. A machine-readable index of the > chapters is at [`/llms.txt`](/llms.txt), and any page is available as raw > Markdown by appending `.md` to its URL. You run one **server** (backed by Postgres) and point many thin **clients** at it, one per machine. The client discovers the session logs Claude Code, Codex, and pi leave on disk, resolves each session's working directory to a canonical git remote, and streams the raw bytes to the server with a resumable, append-only protocol. The server stores those bytes losslessly, parses them into a normalized projection (messages, tool calls, token usage, cost from a compiled-in rate table), and serves a web UI and a read-only [MCP](./agent-access.md) endpoint over it. **Projects** are keyed by git remote, so the same repository across worktrees and machines collapses into one. Because the client keeps no derived state, a parser improvement reaches old sessions by re-parsing on the server, with nothing re-uploaded. Everyone signed in sees every session; you **publish** one to share it with a logged-out viewer. ## In a hurry: get your sessions flowing If your goal is "get my agent sessions into an akari server my team already runs," here is the whole path, each step linked to its detail: 1. **Install the client.** [Getting started](./getting-started.md#1-install-the-client). 2. **Mint an ingest token** on the server's account page and run `akari login --server --token `. [Getting started](./getting-started.md#2-point-the-client-at-your-server). 3. **Push once, then keep pushing.** `akari sync` uploads everything new; `akari watch` (or `akari daemon start`) keeps it flowing. [Getting started](./getting-started.md#3-push-your-sessions). 4. **Read them.** Open the server in a browser, or connect a coding agent over [MCP](./agent-access.md). No server yet? [Self-hosting](./self-hosting.md) stands one up with a single `docker compose up`. ## Read in order The chapters build on each other. 1. **[Introduction](./introduction.md)**: the problem akari solves, the core model (raw bytes in, parsing on the server, keyed by git remote), and what akari is and is not. 2. **[Getting started](./getting-started.md)**: install the client, mint an ingest token, and push your first sessions. 3. **[The client](./the-client.md)**: the `akari` CLI in depth. `login`, `sync`, `watch`, and the `daemon`; how it discovers sessions on disk; and the resumable, append-only upload. 4. **[The web UI](./the-web-ui.md)**: reading your history. The overview and its trailing windows, the Insights quality surface, the session feed with search and filters, projects, and the transcript view with its tool bodies and live updates. 5. **[Accounts and sharing](./accounts-and-sharing.md)**: registration and invites, the three token scopes (`ingest`, `read`, `full`), session visibility, and publishing a session or your usage overview. 6. **[Agent access](./agent-access.md)**: point a coding agent at your history through the read-only Model Context Protocol endpoint. The connect flow and the full tool catalog. 7. **[Self-hosting](./self-hosting.md)**: run the server. Docker Compose, configuration, the database, the first admin account, and reparse. 8. **[Glossary](./glossary.md)**: the terms the guide uses (sessions, projects, the fleet, transcripts, tokens and cost, content-addressed storage, reparse), defined for reference. ## Good to know A few constraints shape everything that follows: - **The client runs anywhere; the server is Linux-only.** Push from macOS, Windows, or Linux; host the server on Linux (a container or a systemd service). - **Supported agents are Claude Code, Codex, and pi.** The client reads the session logs each leaves in its standard location. - **Authorization is deliberately flat.** Signed in means you see every session. There is no private-to-one-user state; sharing is a matter of publishing, not of per-user walls. [Accounts and sharing](./accounts-and-sharing.md) covers the full model. - **A session that did not run in a git repository is still kept**, but keyed to a local folder rather than a shared project. [Glossary](./glossary.md#projects) explains how that resolution works. --- Next: [Introduction](./introduction.md) -> the problem akari solves and the core model. --- # Introduction ## The problem Claude Code, Codex, and pi each write a detailed session log to disk as they work: every message, every piece of thinking, every tool call and its result, the tokens spent. That log is the record of what an agent actually did, and it is worth keeping. But by default it lives in a dot-directory on one laptop, in a format built for the tool that wrote it, with no cost attached and no way to search across the run you half-remember from last week on a different machine. Three things follow from that: - **The record is scattered.** The same repository, worked on from two worktrees or two machines, leaves its sessions in two places that never meet. There is no one history to search. - **The record is ephemeral.** A reinstall, a cleared cache, or a new machine and the trail is gone. Nothing is backed up. - **The spend is invisible.** The logs carry token counts but not dollars, and no rollup tells you where the money and the tokens are going across projects and over time. ## The core idea akari is an explicit **client/server split**. Many thin **clients** push raw session bytes to one **server**; the server does all the parsing, storage, and rendering. Three properties define the split: 1. **Raw bytes in, parsing on the server.** The client does not interpret the logs. It discovers them, resolves each to a git project, and streams the unmodified bytes. The server is the one place that parses, prices, and stores. Because the client keeps no derived state, a better parser reaches every old session by re-parsing the bytes already on the server, with nothing re-uploaded. That rebuild is automatic: a new server binary notices its parser changed and rebuilds in the background. 2. **Keyed by git remote.** A session is filed under the canonical git remote of the directory it ran in. The same repository cloned into several worktrees, or onto several machines, resolves to the same remote and collapses into one **project**. You see all the agent work on a repository in one place, no matter where it happened. A session with no usable remote is still kept, keyed to its local folder instead. 3. **One shared history.** Everyone signed in to a server sees every session on it, with no per-user walls. Sharing a session **publicly** is a deliberate act: you publish it, minting an unguessable link a logged-out viewer can open. Everything else (the cost ledger, the live transcript view, the MCP endpoint an agent reads) is built on those three. ## What akari is and is not What it is: - **A backup and a ledger.** Sessions are stored losslessly and priced, so the record survives a wiped laptop and you can see where tokens and cost go. - **A reading surface for humans and agents alike.** The same history is a web UI you browse and a read-only [MCP](./agent-access.md) endpoint an agent queries. What it is not: - **Not partitioned per user.** Everyone signed in to a server sees every session on it. The history is private to that team, not to the individual who ran each session; there are no per-user walls to manage. To reach someone without an account, you publish a session. - **Not multi-tenant.** One server is one team's shared history. Isolation between teams is one server per team, not per-user partitions inside one. - **Not an agent runner.** akari never runs an agent or writes to your code. It reads the logs your agents already produce. Its MCP surface is read-only by construction. The practical shape of sharing and access is [Accounts and sharing](./accounts-and-sharing.md). --- Next: [Getting started](./getting-started.md) -> install the client and push your first sessions. --- # Getting started This chapter gets you from nothing to a session history flowing to a server. It assumes a server already exists (a teammate runs one, or you do). If you need to stand one up first, [Self-hosting](./self-hosting.md) does it with a single `docker compose up`, then come back here. Terms like *session*, *project*, *ingest token*, and *scope* appear in passing; the [Glossary](./glossary.md) defines each. ## 1. Install the client The `akari` client runs on macOS, Windows, and Linux. The quickest path is the install script: it downloads the release archive for your platform, verifies it against the release `SHA256SUMS`, and puts `akari` on your `PATH`. On macOS and Linux: ```sh curl -fsSL https://raw.githubusercontent.com/jssblck/akari/main/scripts/install.sh | sh akari version ``` On Windows, from PowerShell: ```powershell irm https://raw.githubusercontent.com/jssblck/akari/main/scripts/install.ps1 | iex akari version ``` Set `AKARI_VERSION` (for example `v0.1.0`) to pin a release instead of taking the latest. Prefer to build from source? With a Go toolchain: ```sh go build -o akari ./cmd/akari ./akari version ``` The client updates itself in place later with `akari update` (and `akari update --check` reports whether one is available without installing it). ## 2. Point the client at your server The client authenticates to the server with an **ingest token**: a push-only credential scoped so it can upload sessions and nothing else. It cannot read your history or mint other tokens. Mint one from the server's web UI: 1. Sign in to the server in a browser. 2. Open **Account**, find **API tokens**, and create a token with the **ingest** scope. (The three scopes are `ingest`, `read`, and `full`; see [Accounts and sharing](./accounts-and-sharing.md#api-tokens).) 3. Copy the token. It is shown once. Then hand the server URL and token to `akari login`: ```sh akari login --server https://akari.example.com --token ``` This writes a small config file (server URL and token only) to your OS config directory, with owner-only permissions. That is the client's entire persistent state; it keeps no session bookkeeping of its own. [The client](./the-client.md#configuration) covers the config file and its options in full. ## 3. Push your sessions Do a dry run first to see what the client found and where each session would be filed, without uploading anything: ```sh akari sync --dry-run ``` Each discovered session prints its resolved project (a git remote, or a local folder when the working directory is not a git repository) or its skip reason. When it looks right, push for real: ```sh akari sync ``` `akari sync` discovers the session logs Claude Code, Codex, and pi leave in their standard locations, resolves each to its git project, and streams the new bytes to the server in one pass, then exits. Uploads resume from the server's cursor, so a re-run only sends what is new. By default `sync` stops starting new uploads after five minutes (the file it is on when the limit hits still finishes cleanly); tune it with `--time-limit`, a Go duration such as `30s` or `10m`, or `0` to remove the cap: ```sh akari sync --time-limit 30s # grab a quick sample, then stop ``` ## 4. Keep it flowing `sync` is one-shot. To keep pushing as your agents work, run the watcher, which uploads sessions as they change: ```sh akari watch # foreground; Ctrl-C to stop ``` Or run the same loop in the background and manage it as a per-user daemon: ```sh akari daemon start # launch watch in the background; prints its PID and log path akari daemon status # is it running? akari daemon stop # stop it ``` Run `akari daemon start` once and the watcher keeps uploading in the background. ## 5. Read what you pushed Open the server in a browser. Your sessions appear on: - **Overview**: fleet-wide cost, tokens, and session counts for a trailing window, with an activity heatmap. - **Sessions**: every session in one feed, filterable by agent, project, user, and machine. - **Projects**: one row per git-remote project, with its usage and a cost sparkline. Click any session to read its full transcript. [The web UI](./the-web-ui.md) is the tour. To read the same history from a coding agent instead of a browser, wire up the [MCP endpoint](./agent-access.md). ## When something looks off The most common first-run snags: - **`akari sync` uploaded nothing.** Run `akari sync --dry-run` and read the skip reasons. A session whose working directory is not a git repository is filed under a local folder rather than skipped; a session file the client cannot read a header from is skipped entirely. - **A session went to a "local" project you did not expect.** Its working directory had no usable git `origin` remote (not a repo, no `origin`, several origins, or an unrecognized origin URL), so it was keyed to the machine and folder instead of a shared project. See [Glossary](./glossary.md#projects). - **`akari login` succeeds but `sync` is rejected.** The token is probably not an ingest (or full) scope token, or it was revoked. Mint a fresh **ingest** token and log in again. - **The client cannot find your sessions.** akari looks in each agent's standard location. If yours live elsewhere, add an `extra_roots` entry to the config; see [The client](./the-client.md#discovery). --- Next: [The client](./the-client.md) -> the CLI in depth: how it discovers, resolves, and uploads. --- # The client The `akari` client is the piece that runs on each machine. It finds the session logs your agents write, works out which git project each belongs to, and streams the raw bytes to the server. It keeps almost no state of its own: a config file with a server URL and a token, and nothing else that has to survive a restart. This chapter is the reference for driving it. ## Commands | Command | What it does | | --- | --- | | `akari login --server --token ` | Write the client config (server URL and token). | | `akari sync` | Discover and upload everything new, then exit. | | `akari watch` | Stay running and upload sessions as they change (foreground). | | `akari daemon start` \| `status` \| `stop` | Run and manage `watch` as a background process. | | `akari update` | Update the client to the latest release in place. | | `akari version` | Print the build version and exit. | Every command takes `--config ` to point at a config file other than the default. A first `Ctrl-C` stops `sync` from starting new files (the one in flight finishes) and winds `watch` down gracefully; a second exits at once. ### login ```sh akari login --server https://akari.example.com --token [--machine ] ``` `login` writes the server URL and API token to the config file and exits. The token is minted out of band on the server (its account page) and passed in; `login` does not create it. Use an **ingest**-scope token for a push-only client, or a **full**-scope token if the same credential also drives the web API. It preserves any `extra_roots` and `excludes` already in the config, so re-running it to rotate a token or move servers does not wipe your discovery settings. `--machine ` sets the logical machine name this client reports for every session (see the `machine` config key below). Omit it to keep the OS hostname, or to leave an existing name untouched on a re-run; pass `--machine ""` to clear it back to the hostname. ### sync ```sh akari sync [--dry-run] [--time-limit ] [--concurrency ] [--finalize] ``` `sync` makes one pass: discover every session file, resolve each to a project, upload what is new, and exit. Because uploads resume from the server's cursor, a re-run sends only bytes the server does not already have. - `--dry-run` resolves and reports what would upload, with a skip reason for each file it would not, and uploads nothing. Run it first when setting up a machine. - `--time-limit ` caps how long `sync` keeps *starting* new uploads, a Go duration such as `30s` or `5m` (default `5m`; `0` removes the cap). The limit gates only when new work begins, so the file being uploaded when the limit elapses runs to a clean stopping point rather than being abandoned mid-stream. Because uploads resume, repeated short runs ingest a backlog in chunks. - `--concurrency ` bounds how many files upload in parallel (default the CPU count, capped at 8). Each file also parallelizes its own body uploads under a shared limiter, so the file-level cap stays modest on purpose. A given file never races with itself. - `--finalize` treats every session as terminal, flushing each one's final turn now instead of waiting for the file to go idle (see "How the upload works" below). It also tells the server the session is finished: the announce marks it terminal and, once the whole transcript has landed, the client asks the server to grade it immediately rather than waiting out the server-side settle window (30 minutes idle). So on an ephemeral host the quality grade is available at the end of the run, in time to report or gate on, instead of long after the host is gone. Use it on a host that disappears right after the sync, a CI job or a cloud sandbox, where neither wait would elapse and the last turn would otherwise never upload and the grade would never land. Reach for it only when every session is genuinely finished: on a workstation where a session may still be running, it would flush a turn mid-stream, so let the idle wait do its job there instead. ### watch ```sh akari watch ``` `watch` runs in the foreground and uploads sessions as they change, logging to standard error. It holds a single-instance lock for its lifetime, so two watchers cannot run at once. Under the hood it layers three change detectors so nothing is missed: an OS file-system watcher for prompt, debounced uploads; a periodic re-stat of known files to catch changes the OS watcher drops (network filesystems, watch exhaustion); and a slower full rescan that rediscovers roots for newly created files. It does an initial full pass before entering the event loop, ingesting any backlog on startup. ### daemon ```sh akari daemon start # launch watch in the background; prints its PID and log path akari daemon status # report whether it is running, and its PID akari daemon stop # stop the running watcher ``` `daemon` runs the same `watch` loop as a detached, per-user background process (it is not a system service). It writes a pidfile and a log file under your config directory; `start` confirms the child took the single-instance lock before returning, and `stop` verifies a live instance holds it before signaling. This is the steady state on a workstation: run `akari daemon start` once. ### update and version ```sh akari update # update to the latest release in place akari update --check # report whether an update is available, install nothing akari update --force # reinstall the latest even if already current akari version # print the build version ``` `update` is a native updater: it downloads the latest release archive for your platform, verifies it against the release `SHA256SUMS`, and swaps the binary in place, with no shell or `curl` involved. On Windows it moves the running executable aside so the update succeeds while akari is running; restart any `akari watch` or `akari daemon` afterward to pick up the new version. ## Configuration `akari login` writes the config; you can also edit it by hand. It is a TOML file in your OS config directory: | Platform | Path | | --- | --- | | macOS | `~/Library/Application Support/akari/config.toml` | | Linux | `~/.config/akari/config.toml` | | Windows | `%AppData%\akari\config.toml` | Pass `--config ` to any command to use a different file. A full example: ```toml server_url = "https://akari.example.com" token = "akari_ingest_..." # Report every session under one stable machine name instead of the hostname. machine = "sandbox-pool" # Discover sessions from extra locations, beyond each agent's standard root. [[extra_roots]] agent = "claude" path = "/mnt/shared/claude-sessions" # Skip paths matching these globs during discovery, for sync and watch alike. excludes = ["**/scratch/**", "*.private.jsonl"] ``` The keys: - **`server_url`** (required): the server's base URL, no trailing slash. - **`token`** (required): the API token, used as a bearer credential. Ingest scope is the right choice for a push-only client. The file is written with owner-only permissions, since it holds a credential. - **`machine`** (optional): the logical machine name reported for every session this client uploads. Empty falls back to the OS hostname. Set it to give a fleet of ephemeral or containerized hosts (CI jobs, autoscaled workers, throwaway dev containers) one stable identity such as `ci` or `sandbox-pool`, so the machine filter does not fill with thousands of single-use hostnames. `AKARI_MACHINE` overrides it per run. - **`extra_roots`** (optional): additional discovery roots, each an `{ agent, path }` pair where `agent` is `claude`, `codex`, or `pi`. Use these when your sessions live somewhere other than the standard location. - **`excludes`** (optional): glob patterns of paths to skip, applied to both `sync` and `watch`. Patterns match the full path with `/` separators; `**/scratch/**` ignores any path with a `scratch` segment, `*.private.jsonl` excludes by suffix. Empty discovers everything. ## Machine identity Every session records the **machine** it came from, a dimension you can filter the feed and each project by. By default that is the OS hostname. On a workstation that is exactly what you want; on ephemeral or containerized hosts it is not, because each run gets a distinct one-off hostname and the machine filter fills with thousands of single-use values that mean nothing. Give such a fleet one stable logical machine instead. The name is resolved from three sources, highest priority first: 1. the **`AKARI_MACHINE`** environment variable, a per-run override that needs no config file (the easy path for a container that sets env far more readily than it writes config); 2. the **`machine`** config key, set at `akari login --machine ` or by hand, for a stable per-host or per-fleet name; 3. the **OS hostname**, the default when neither is set. The sessions still aggregate by project, user, and agent exactly as before; only the machine label changes, so an ephemeral fleet reporting as `ci` shows up as one machine rather than polluting the rail. `AKARI_MACHINE` is the only environment variable akari itself defines. The client also honors each agent's own root override for discovery (see below). ## Discovery On every run the client looks for session files in each agent's standard location, plus any `extra_roots` you configured: | Agent | Default root | Override | | --- | --- | --- | | Claude Code | `~/.claude/projects` | `CLAUDE_PROJECTS_DIR` | | Codex | `~/.codex/sessions` and `~/.codex/archived_sessions` | `CODEX_SESSIONS_DIR` | | pi | `~/.pi/agent/sessions` | `PI_DIR` (sessions at `$PI_DIR/agent/sessions`) | Missing roots and excluded paths are skipped without error. For each candidate file, the client peeks the first line to read the working directory and session id, then resolves that directory's git `origin` remote to a project key. A file whose header cannot be read is skipped entirely; a directory with no usable remote produces a standalone or orphaned project rather than being dropped ([Glossary](./glossary.md#projects)). ## How the upload works The upload protocol is not something you invoke directly, but its shape explains the client's behavior. The upload is **resumable** and **append-only**, and the client is stateless across runs: 1. **Announce.** The client tells the server about a file (its agent, source id, project, branch, machine). The server replies with how many bytes it already holds for that session and a digest of that verified prefix. 2. **Reconcile.** The client hashes its local file up to the server's cursor. If the digest matches, the prefix is verified and the client resumes from there. If it does not (the file was truncated, rewritten, or rotated), the client resets the session and re-uploads from the start. 3. **Stream.** The client streams the remaining bytes as newline-aligned chunks. As it goes, it lifts large tool bodies out of the transcript into content-addressed storage: it checks whether the server already holds each body by hash, uploads the ones it does not, and leaves a small reference in the stream. The server re-parses the session moments after chunks land, so a live session appears to grow in the UI as it runs. Two consequences worth knowing: - **A session's final turn is withheld until its file goes idle.** The last turn often has no closing line to mark it complete, so the client waits for the file to be untouched briefly before flushing it. On a host that is torn down right after the sync (CI, a cloud sandbox), that idle wait never elapses, so pass `akari sync --finalize` to flush the final turns immediately. `--finalize` also marks each session terminal on the server so its quality grade is computed at the end of the run rather than after the server's own 30-minute settle window, which the host would not be around to see. - **Re-running is cheap and safe.** Because the server tracks the cursor and the client re-derives everything from the file, `sync` after `sync` uploads only new bytes, and an interrupted upload resumes rather than restarting. --- Next: [The web UI](./the-web-ui.md) -> reading the history you just pushed. --- # The web UI The web UI is where a human reads what the agents did. It is server-rendered: a persistent left sidebar carries the primary sections (Overview, Insights, Projects, Sessions, Account, and this Guide), with the signed-in user and a log-out control at its foot. Reading the UI needs a full-scope credential, which in practice is a browser session; signing in gives you that. The site root (`/`) is the public homepage, shown to everyone; signing in takes you to the app, whose home is the Overview at `/overview`. Every timestamp renders in your own timezone: the browser reports its zone in a cookie once, and the server formats against it from the next page on. ## Overview **Overview** (at `/overview`) is the app's home: fleet-wide usage bounded to a trailing window. Pick the window (7, 30, or 90 days, a year, or all of history) and every figure on the panel follows it: - **Cost, combined tokens, and session totals** for the window, as stable tabular figures. A partial cost (some session used a model the rate table does not price) shows a trailing `+`. - **A daily-activity heatmap**, one cell per day, so a busy stretch or a quiet one is visible at a glance. - **By-model and by-agent breakdowns** of where the usage went, plus a by-user breakdown once more than one account has usage in the window. You can also scope the overview to specific accounts. ## Insights **Insights** reads how the window's sessions went, where the Overview reads what they cost. The same trailing-window selector bounds every panel, and the toolbar shows how many sessions the window holds. Top to bottom: - **Concurrency and velocity**: how many sessions ran at once at the fleet's peak (and when), the busiest single user, and the average; then how fast turns cycled: the median and slow-tail (p90) response latency, the opening reply on its own, and messages and tool calls per active minute. - **Tools**: total calls, the fleet-wide error rate, and calls per turn, over a list of the busiest tools, each bar sized by call volume and colored by its own reliability. - **Prompt hygiene**: how clearly the window's prompts set the agent up: the share that were terse, repeated, or carried no code pointer, and the sessions that opened unstructured. These rates describe the human's input and never move a grade. - **Context health**: how heavy sessions got (the median, p90, and heaviest peak context, as raw token counts) and how often they shed context (the share of sessions with at least one inferred reset, and the total). A load measure, not spend. - **People**: one row per author, busiest first: session count, an outcome-mix bar (hover it for the counts), how many of their sessions carry a grade, and their average score. A name links to that author's sessions. The table appears only when two or more authors ran sessions in the window; a single author would just restate the fleet figures. - **Grades**: the quality score banded A to F, with an **unscored** bucket for sessions that carry no grade. A note on the panel says what share of the window is graded (for example "62% graded"), so you can weigh the distribution by how much it speaks for. - **Outcomes**: how each session ended. **Completed** means the agent had the last substantive word with nothing left hanging; **abandoned** means the human walked away without a reply or interrupted a tool; **errored** means it stopped on failing tool calls or died mid-tool with no human in the loop; **unknown** means there is no verdict yet: the session is still live, or there was nothing to read. - **Archetypes**: what kind of session each was, by length and turn count. **Quick** is a short exchange, **standard** an ordinary working session, **deep** a long and involved one, **marathon** an exceptionally long or message-heavy one, and **automation** a run with no human turn at all (a subagent or a scripted job). - **File churn**: the files edited more than once in the window, most-edited first, each with its edit count and how many sessions returned to it. Rows are grouped per project across worktrees, so the same repository file edited from several checkouts reads as one row, tagged with its project. Every grade and outcome bar links into the Sessions feed filtered to that bucket, so the count you see is the list you land on. A session is graded only after it settles: half an hour idle, so no verdict is taken on a run that may still be moving. A session that just finished reads as unscored (and its outcome as unknown) until then. ## Sessions **Sessions** is every session across every project in one feed, so you can find a run without first picking its project. A slim toolbar narrows the feed by **agent**, **project**, **user**, and **machine**, and sorts it by recency, token volume, message count, or cost; active filters show as removable chips. A search box narrows to sessions whose transcript contains the query, composing with every other filter; a matching row shows a snippet with the hit highlighted. Every other row carries its **first-prompt title**, the opening line of what the session was asked to do. The feed loads a page at a time with a **Show more** control, and sessions that parsed to no messages are hidden behind a toggle. This is the place to answer "where is that run I did last Tuesday." A toolbar above the feed adds **outcome** and **grade** filters (how a session ended, and its letter grade), the same buckets the [Insights](#insights) distributions and the project view's quality panels break down; clicking a bar on either lands you on this feed already filtered to it. ## Projects The **Projects** index is one full-width table of git-remote projects. Each row carries the project's session count, a single token total (hover it for the input/output/cache-read/cache-write breakdown), its cost, a 30-day cost **sparkline**, and a relative "updated" time. Fleet-wide usage lives on the Overview, and standalone or orphaned local folders reach you through the Sessions project filter, so neither crowds this table. Click a project for the **project view**: that project's sessions across all users and machines, with agent, user, and machine filters and the same analytics panel as the overview, scoped to the project and its trailing window. A **Quality band** below it breaks the same scope down by grade, outcome, and archetype, with tool reliability and a churn list of files edited more than once; the grade and outcome bars link into this project's filtered sessions. ## The session view Clicking a session opens the deep read. A sticky stats header keeps the session's gauges in view as you scroll: tokens in, out, and cached; cost; duration; message counts; and a **Quality tile** carrying the session's grade and outcome, which reveals a score-arithmetic breakdown (each penalty and its points) on hover. Below it is the transcript itself: - **Messages, thinking, and tool calls**, in order, with a timeline rail that maps the turns and flags any tool that errored. Each turn carries a reply-latency stamp, a per-message context size (for example "ctx 82k"), and a cost stamp whose tooltip breaks the cost down by token class; a divider marks a context shed (for example "context shed: 356k -> 66k"), the sharp drops that read as a compaction or a clear. A user message carries a prompt-hygiene badge (terse, no code pointer, repeat) where it applies. - **Tool bodies as chips.** A tool call's input and result show as size-and-type chips (for example "36 KB json"). Clicking one opens the body in the inspector modal, fetched from content-addressed storage on demand, so a large body gets real room without pushing the transcript around. An editing tool's input opens as a rendered **diff** rather than raw JSON. A chip's file path shows worktree-relative rather than absolute; a tool with no file path, such as a shell command or a search pattern, instead carries a one-line summary of its input on the chip, with the full text on hover. - **Subagents** spawned by the session are listed under it, so a run that launched helpers reads as a tree rather than scattered rows. - **Live updates.** A session still being written updates in place over server-sent events as new bytes are parsed, so you can watch a run unfold. From the session page its owner (or an admin) can publish, unpublish, or delete it; [Accounts and sharing](./accounts-and-sharing.md#sharing-a-session) covers what each does. ## Account The **Account** page is your control surface: - **API tokens**: create and revoke tokens in the `ingest`, `read`, or `full` scope. The plaintext token is shown once, at creation. - **Connected apps**: the coding agents you have connected over [MCP](./agent-access.md), each with a one-click disconnect that revokes its tokens at once. - **Publicity**: publish or unpublish your own usage overview at `/u/`. - **Invites** (admins only): mint an invite token for a new teammate, and see every invite ever issued with its status (unused, redeemed by whom, or expired) and a revoke control for the ones still open. - **Reparse** (admins only): force a rebuild of the parsed projection, with a live progress bar. The Account page stays available during a reparse, since it is not parsed data and it hosts this control. --- Next: [Accounts and sharing](./accounts-and-sharing.md) -> registration, the three token scopes, and publishing. --- # Accounts and sharing akari's authorization is deliberately flat. Signed in to a server, you see every session on it; there is no per-user wall to reason about. What this chapter covers is the thin layer around that: how accounts are created, the three token scopes that grant machine access, and how you deliberately share something outward. ## Accounts and invites Membership is closed and invite-gated, with one exception that bootstraps a fresh server: - **The first account is the admin.** The very first account registered on a new server needs no invite and becomes an admin. This is how a new server gets its first user. - **Every later account needs an invite.** Registration otherwise requires a valid, unredeemed invite token. An admin mints one from the Account page and hands it to the new teammate, who enters it when registering. An invalid, already-redeemed, or expired token is rejected. - **Admins** can mint invites, force a [reparse](./self-hosting.md#reparse), and delete any session. A normal user manages only their own sessions and tokens. You sign in with a username and password; a successful login sets an `HttpOnly`, `SameSite=Lax` session cookie (marked `Secure` unless the server is running in plain-HTTP development mode). Only a hash of the session secret is stored, the same discipline as tokens and invites. Logging out deletes the session and clears the cookie. ## API tokens Beyond the browser session, akari issues **API tokens**: bearer credentials for machines. Every token has one of three **scopes**, and the scope alone determines what the token can do. Create and revoke them from the Account page; the plaintext is shown once, at creation, then only its metadata (name, scope, last used) is visible. | Capability | `ingest` | `read` | `full` | | --- | --- | --- | --- | | Push sessions and blobs | yes | no | yes | | Read the web UI | no | no | yes | | Reach the [MCP](./agent-access.md) endpoint | no | yes | yes | | Publish / unpublish / delete, mint tokens | no | no | yes | The intent behind each: - **`ingest`** is push-only. It is what the [client](./the-client.md) uses: it can upload sessions and their blobs and nothing else, so it is safe to leave on a laptop or bake into a deployment. - **`read`** is read-only. It sees everything a signed-in user sees but can mutate nothing (no publish, delete, or token creation), which is exactly what you want to hand an untrusted coding agent. It is the scope the OAuth connect flow issues. A full-scope token also reaches the MCP endpoint, but it carries the whole write surface with it, so `read` is the one to hand out. - **`full`** is read and write: the browser session's level of access, as a token. Treat it as a real credential; it is rarely the right thing to hand a third party. A useful consequence: the server-rendered UI requires a full-scope credential, so pointing an ingest- or read-scope token at a browser page just bounces to the login screen. ## Session visibility A session's visibility is **internal** by default: any signed-in user of the server can read it. There is no state below that; on one server, signed in means you see everything. ### Sharing a session From a session's page, its owner can: - **Publish** it, which mints an unguessable public id and serves the session at `/s/` to logged-out viewers. The public page never exposes the numeric session id, and it links only to subagents that are themselves published, so publishing a parent does not leak its children. - **Unpublish** it, which clears the link at once; the old URL stops resolving. - **Delete** it (owner or an admin). Deleting cascades the transcript and the raw bytes; any content-addressed blobs it referenced are reclaimed by the next background sweep, unless another session still points at them. Published or not, a tool body is always fetched through a session that references it, so a public link exposes only that session's own bodies, never an internal one that happens to share a hash. A published session also unfurls with an Open Graph preview card at `/s//og.png`: the session's title, an activity strip that plots the session's own usage over its span (the session card's take on the overview heatmap, each cell a slice of the run's length), and four foot figures (total tokens, message count, its quality grade, and its duration), rendered in the house style as a pure-Go PNG. A figure with nothing to show (an unscored session's grade, or an undated session's duration) reads as a dash rather than a misleading zero. Like the overview card below it is rendered on demand the first time it is fetched and served from cache until the TTL expires. ### Sharing a project's overview Any signed-in user can publish a **project's usage overview** at `/p/` from the project's page. Projects are shared across the whole server rather than owned, so there is no per-owner gate: the public page is the same aggregate panel and quality band the signed-in project page shows (totals, the activity heatmap, the by-model and by-agent breakdowns, and the grades/outcomes/tools band) scoped to that one repo. It lists no sessions and names no accounts, so it shares the repo's usage shape without exposing a session or which people ran in it. The address is the project id, so unpublishing hides the page without changing the link. Like the user overview, a published project overview gets an Open Graph preview card at `/p//og.png`, the same simplified heatmap in the house style, with three foot figures: total tokens, session count, and a single representative quality grade (the mean score across the project's graded sessions, rounded to a letter). The grade reads as a dash when no session in scope is scored. ### Sharing your usage overview From the Account page's Publicity control you can publish your own **usage overview** at `/u/`. The public page is the same aggregate panel you see (totals, the activity heatmap, the by-model and by-agent breakdowns) scoped to your account alone: it carries no session links and no one else's numbers, so it shares your usage shape without exposing any session or any other user. The address is your username, so unpublishing hides the page without changing the link, and re-publishing brings the same URL back. A published overview also gets an Open Graph preview card at `/u//og.png`, so a shared link unfurls with an image: a simplified copy of your activity heatmap plus the headline figures, rendered in the house style as a pure-Go PNG (no headless browser). All three per-entity cards (this one, the project overview's, and a session's) work the same way: rendered on demand the first time the card is fetched (typically when a share unfurls), then served from cache until the TTL expires ([Self-hosting](./self-hosting.md#configuration) documents it, an hour by default); the next fetch after that renders a fresh one. A background sweep prunes expired cards. A card may trail the live page by up to the TTL. The overview and project cards represent the default trailing-year window, so a link viewed under a narrower range carries no image rather than a mismatched one. ## Connected agents When you connect a coding agent over MCP, that connection appears on the Account page under **Connected apps**, with when it connected and when it was last used. Disconnecting one revokes every token it holds at once. The connection itself is a read-only OAuth grant; the mechanics are [Agent access](./agent-access.md). --- Next: [Agent access](./agent-access.md) -> reading your history from a coding agent over MCP. --- # Agent access akari serves a remote [Model Context Protocol](https://modelcontextprotocol.io) endpoint, so a coding agent can read your whole session history without opening a browser. It exposes the same surface the web UI shows (the overview analytics, the projects index, the session feed, and a session's full transcript) plus the raw data behind it: tool-call bodies from the content store, and the lossless bytes a session was ingested from. It is **read-only** by construction; no tool creates, changes, or deletes anything. The endpoint is at `/mcp` on your server, over Streamable HTTP: ``` https://akari.example.com/mcp ``` ## Connecting with a browser (recommended) Connect it once from your harness. In Claude Code: ```sh claude mcp add --transport http akari https://akari.example.com/mcp ``` On first use the harness opens your browser to akari, which recognizes the session you are already signed in to and asks you to approve the connection. The browser sign-in is the authentication; no credential is passed to the agent. Behind that click is the OAuth 2.1 flow MCP defines, with akari acting as both the resource and the authorization server. The agent registers itself, redirects through a PKCE-protected authorization request, and exchanges the result for a read-only access token that refreshes on its own. The token carries the **read** scope and nothing more. You can revoke it any time from the Account page's **Connected apps** section, which disconnects the agent and invalidates its tokens at once. For the flow to advertise correct URLs behind a reverse proxy, set `AKARI_PUBLIC_URL` to the server's external origin ([Self-hosting](./self-hosting.md#configuration)). ## Connecting without a browser A harness that cannot run the browser flow authenticates with a **read-scope API token** instead. Create one on the account page (the `read` scope is read-only, the counterpart of the push-only `ingest` and the read-write `full`) and pass it as a bearer token: ```sh claude mcp add --transport http akari https://akari.example.com/mcp \ --header "Authorization: Bearer " ``` A read token reaches only the MCP endpoint: it cannot push sessions or drive the write surface. It does not expire until you revoke it. ## The tools Every tool is read-only and sees every internal session, the same surface a signed-in user sees. Fetch data top-down: `overview` and `list_projects` for the lay of the land, `list_sessions` to find runs, `get_session` for a transcript, then `read_tool_body` or `get_session_raw` to go deeper on one. | Tool | Returns | | --- | --- | | `whoami` | The account the credential authenticates as: user id, username, and whether it is an admin. | | `overview` | Fleet usage for a trailing window: cost, tokens by class, session count, a daily series, and by-model and by-agent breakdowns. Also lists the accounts present, so their ids can scope later calls. | | `list_projects` | Every project, most recently active first, each with its session count and token and cost totals. | | `get_project` | One project's identity, its windowed analytics (optionally narrowed by agent, user, or machine), and the agents, users, and machines that ran in it. | | `list_sessions` | The cross-project session feed with filters and a facet rail, paged. | | `get_session` | One session's header and a window of its transcript: messages, thinking, tool-call metadata, attachments, and subagents. | | `read_tool_body` | A tool call's input or result body from the content store, by the hash the tool call carries. | | `get_session_raw` | The lossless bytes a session was ingested from, behind the parsed projection. | Parameters that govern paging through a large history: - **Trailing windows.** `overview`, `get_project`, and `list_sessions` take `days`; `0` or omitted means all of history. - **Paging the feed.** `list_sessions` returns up to 500 rows and a `next_cursor`; pass it back as `cursor` to walk the whole feed. It also returns a facet rail (busiest agents, users, machines, projects) whose values are the exact strings to pass back as filters. - **Paging a transcript.** `get_session` returns a bounded window of messages (set `include_transcript: false` for just the header). When `transcript.has_more` is true, pass the window's `next_after` as `transcript_after` to fetch the next page. - **Fetching bodies.** Tool bodies are not inlined in `get_session`; take the `input_sha256` or `result_sha256` off a tool call and pass it, with the `session_id` that references it, to `read_tool_body`. Text returns as text, binary as base64, capped by `max_bytes`. ## What the MCP sees The MCP surface mirrors the web UI: the same sessions, the same visibility rule (every internal session, exactly what a signed-in user sees), plus `get_session_raw` for the ingested bytes, which the web UI does not surface. It exposes no account or token management and no way to publish, delete, or write; those stay on the full-scope web surface. --- Next: [Self-hosting](./self-hosting.md) -> run the server yourself. --- # Self-hosting The akari server is a single Linux binary backed by Postgres. It embeds its own UI, fonts, and database migrations. This chapter covers standing it up, configuring it, and the operations it needs over its life. (The client that pushes sessions to it runs anywhere and is [its own chapter](./the-client.md).) ## Running the server ### With Docker Compose The bundled `docker-compose.yml` brings up Postgres and the server together and is the quickest way to a running instance: ```sh docker compose up -d --build ``` It starts Postgres 18 and the server, which applies its migrations on startup and listens on `:8080`. The compose file runs in plain-HTTP development mode (`AKARI_COOKIE_INSECURE=1`) and ships throwaway database credentials; change both before exposing it. For anything real, terminate TLS at a reverse proxy in front (see [Production](#production)) and point the server at a Postgres you manage rather than the bundled container. ### With the install script (systemd) On a Linux host, the server install script downloads a checksum-verified binary and, with `--systemd`, wires it up as a managed service: ```sh curl -fsSL https://raw.githubusercontent.com/jssblck/akari/main/scripts/install-server.sh | sh -s -- --systemd ``` That installs a dedicated `akari` user, a `akari-server` systemd service, and an environment file at `/etc/akari/server.env` where you set the configuration below. Manage it the usual way: ```sh sudo systemctl start akari-server sudo systemctl status akari-server sudo systemctl restart akari-server ``` ### From source With a Go toolchain and a reachable Postgres: ```sh go generate ./... # regenerate the templ UI (gitignored) go build -o akari-server ./cmd/akari-server export AKARI_DATABASE_URL="postgres://akari:akari@localhost:5432/akari?sslmode=disable" ./akari-server ``` The server applies its embedded migrations on startup, so there is no separate migration step, and a restart is always safe. ## Configuration The server is configured entirely from the environment; there is no config file. Only the database URL is required. | Variable | Default | Meaning | | --- | --- | --- | | `AKARI_DATABASE_URL` | (required) | Postgres connection string, for example `postgres://akari:akari@localhost:5432/akari?sslmode=disable`. | | `AKARI_LISTEN` | `:8080` | Address the HTTP server binds. Falls back to `PORT` when unset. | | `AKARI_PUBLIC_URL` | (derived) | The externally reachable base URL (`https://akari.example.com`), used as the OAuth issuer and the base of the URLs the [MCP](./agent-access.md) authorization flow advertises. Falls back to `AKARI_URL`; when neither is set the server derives the origin per request, which is correct for a single-origin deployment behind a proxy that forwards the host. | | `AKARI_COOKIE_INSECURE` | unset | Set truthy to drop the `Secure` flag on session cookies, for plain-HTTP local development. Leave unset in production so cookies are HTTPS-only. | | `AKARI_PROXY_AUTH_HEADER` | unset | Enables reverse-proxy single sign-on. The request header a trusted proxy in front sets to the authenticated username (for example `X-Auth-Request-Preferred-Username`). When set, akari trusts that header as the signed-in user and provisions the account on first sight. Leave unset for a direct, locally-authenticated deployment. See [Single sign-on behind a trusted proxy](#single-sign-on-behind-a-trusted-proxy). | | `AKARI_PROXY_AUTH_SECRET` | unset | Optional shared secret the proxy must echo (in `AKARI_PROXY_AUTH_SECRET_HEADER`) for the identity header to be trusted. Defense in depth for when network isolation alone is not enough. Only consulted when `AKARI_PROXY_AUTH_HEADER` is set. | | `AKARI_PROXY_AUTH_SECRET_HEADER` | `X-Akari-Proxy-Secret` | The header carrying `AKARI_PROXY_AUTH_SECRET`. Only consulted when that secret is set. | | `AKARI_SWEEP_INTERVAL` | `1h` | How often the server reclaims orphaned content-addressed blobs. A Go duration (`30m`, `2h`); `0` disables the background sweep. | | `AKARI_OG_CACHE_TTL` | `1h` | How long a rendered Open Graph preview card of a published overview is served from cache before the next request re-renders it. A Go duration; must be positive. | | `AKARI_OG_CLEANUP_INTERVAL` | `24h` | How often the server prunes expired preview cards (older than `AKARI_OG_CACHE_TTL`) from the cache. A Go duration; `0` disables the sweep. | | `AKARI_SIGNALS_SETTLE_INTERVAL` | `5m` | How often the server computes per-session quality signals (outcome, grade, prompt hygiene, context health) for sessions that have settled: a session is graded once it has been idle past the abandoned threshold (30 minutes), off the ingest path, so a live session is never graded with a verdict that would drift. A session an ephemeral host declared terminal (`akari sync --finalize`) is graded immediately instead, both by this pass and by the finalize call the client makes at the end of the sync, so the grade lands before the host is torn down. A Go duration; `0` disables the background pass (signals then land only on reparse, the finalize call, or `akari-server settle`). | ## The database akari stores everything in Postgres: raw session bytes, the parsed projection, user accounts, tokens and invites, and content-addressed blobs (as large objects). Postgres 18 is what the compose file and CI use. Migrations are embedded in the binary and applied on every startup: the server records each applied migration and runs only the new ones, each in its own transaction, so restarts and upgrades need no manual database step. Back it up like any Postgres database on your normal schedule; a standard `pg_dump` that includes large objects captures the blobs along with everything else. ## The first account Registration is closed and invite-gated, with one bootstrap exception: **the first account registered on a fresh server needs no invite and becomes the admin.** Open the server in a browser and register to claim it. That account can then mint invite tokens (Account page) for everyone else, who redeem them when they register. The full account and token model is [Accounts and sharing](./accounts-and-sharing.md). ## Single sign-on behind a trusted proxy akari's built-in accounts are local: a username and password per person, invite-gated after the first admin. To run akari inside an environment that already has its own identity (as a sidecar to another application, or behind your organization's gateway), it can instead trust identity asserted by a reverse proxy in front of it. This is the standard identity-aware-proxy pattern: the proxy authenticates the user against your identity provider, and akari trusts the username it forwards. ### How it works Put an authenticating proxy (oauth2-proxy, Pomerium, or your own gateway) in front of the server. The proxy signs the user in against your IdP and forwards their username in a request header. Set `AKARI_PROXY_AUTH_HEADER` to that header's name, and the server will: - read the username from that header on every request, - provision an account the first time it sees a new one (with no password, and not an admin), and - treat the request as that signed-in user at full scope, exactly like a browser session. Accounts created this way are federated: they have no local password, so the [login form](./accounts-and-sharing.md) refuses them. Their only way in is through the proxy. Everything else (the feed, projects, publishing, and minting API and [MCP](./agent-access.md) tokens) behaves the same as for a local account. Because the proxy authenticates every request, deep-linking a user straight into a page needs no extra step: a link from your other application to `https://akari.internal/sessions/123` arrives already authenticated as whoever the proxy says the user is. ### The trust boundary Turning this on means akari believes anyone who can set the identity header. That is safe only when akari is reachable **exclusively** through the proxy that sets it: a private network, a sidecar sharing a pod, or an ingress that always injects the header. Never expose a proxy-auth instance directly to a network where a client could set the header itself. Configure the proxy to overwrite (not append) the identity header, so a client cannot smuggle its own value through. For defense in depth, set `AKARI_PROXY_AUTH_SECRET` to a value shared out of band with the proxy. The proxy must echo it in `AKARI_PROXY_AUTH_SECRET_HEADER` (default `X-Akari-Proxy-Secret`), or akari ignores the identity header, so a client that reaches the server directly cannot forge an identity without also knowing the secret. It hardens the boundary; it does not replace network isolation. ### Bootstrapping the admin A proxy-provisioned account is never an admin, and once any account exists local registration is invite-only (which needs an admin to mint the invite). So create the first admin through local password registration **before** you enable proxy auth: register in a browser to claim the bootstrap admin (see [The first account](#the-first-account)), then set `AKARI_PROXY_AUTH_HEADER` and restart. Enable proxy auth on a truly empty database and the first proxied request creates an ordinary non-admin account, leaving no admin to mint invites or run a reparse. ### Example With oauth2-proxy in front, forwarding the authenticated username to the akari upstream it protects: ```sh # oauth2-proxy is configured to pass the signed-in user to its upstream, e.g. # --pass-user-headers (sends X-Forwarded-Preferred-Username / X-Auth-Request-*) # Tell akari which of those headers carries the username: AKARI_PROXY_AUTH_HEADER=X-Auth-Request-Preferred-Username ``` Point oauth2-proxy's upstream at the akari server, and make sure only the proxy can reach akari's `AKARI_LISTEN` port (a private network or a shared pod). The exact header name and the flag that emits it vary by proxy and version, so match `AKARI_PROXY_AUTH_HEADER` to whatever your proxy actually sends. Native OIDC login (akari as a relying party, provisioning users on first login) and SCIM provisioning are planned, so you will be able to point akari straight at an identity provider and manage the account lifecycle from it. Until then, the reverse-proxy pattern above is the supported integration. ## Reparse The server keeps each session's raw bytes and a projection parsed out of them, and can rebuild the projection from the bytes at any time (a **reparse**). It runs one on its own when its parser changes: a new binary compares a compiled-in parser epoch against the epoch the stored data was built under and, when they differ, reparses in the background on startup while it keeps serving. There is no manual step after a parser upgrade. You can also force one: - **From the Account page**, an admin can trigger a reparse and watch its progress on a live bar. - **From the CLI:** ```sh akari-server reparse # rebuild every projection from stored raw bytes akari-server reparse --agent claude # limit to one agent ``` While a reparse runs, the parsed pages show a progress notice instead of a half-rebuilt view; the Account page and raw-byte reads stay available throughout. A reparse sweeps orphaned blobs when it finishes. ## Maintenance subcommands The server binary carries a few operational subcommands beside the default run-the-server behavior: ```sh akari-server # run the HTTP server (default) akari-server reparse # force a projection rebuild (see above) akari-server sweep # reclaim orphaned content-addressed blobs now akari-server settle # compute quality signals for every settled session now akari-server dev-seed # fill a local server with example data (development) akari-server update # update to the latest release in place akari-server version # print the build version and exit ``` `sweep` is the manual form of the periodic blob reclaim; it is safe to run any time, since blob liveness is computed rather than reference-counted. `settle` is the manual form of the periodic signals pass: it grades every settled session missing a current-version signals row, then exits. `update` downloads and swaps in the latest release (and reminds you to `systemctl restart akari-server` when a service is installed); inside a container, rebuild the image and redeploy rather than updating the binary in place. `dev-seed` is a development convenience: it creates a few demo accounts (sign in as `grace`, the admin, with password `akari-dev`) and ingests this machine's real agent sessions. It is idempotent (a no-op once the store holds sessions) and best-effort by default. Keep it away from any server holding real data. ## Production A short checklist for a real deployment: - **Terminate TLS at a reverse proxy** (nginx, Caddy, and the like) in front of the server, which itself speaks plain HTTP. Forward to its `AKARI_LISTEN` address. - **Set `AKARI_PUBLIC_URL`** to the external HTTPS origin so the MCP OAuth flow advertises correct URLs. - **Leave `AKARI_COOKIE_INSECURE` unset** so session cookies are marked `Secure` and ride only over HTTPS. - **Point `AKARI_DATABASE_URL` at a managed Postgres**, not the bundled container, and back it up on your normal schedule. - **Capture logs** through your container runtime or systemd; the server logs to standard output and error. - **If you use reverse-proxy single sign-on**, make sure the server is reachable only through the proxy that sets the identity header (see [Single sign-on behind a trusted proxy](#single-sign-on-behind-a-trusted-proxy)). The server shuts down gracefully on interrupt: it drains in-flight requests and lets background work (sweep, card refresh, any reparse) wind down before the connection pool closes. --- Next: [Glossary](./glossary.md) -> the terms the guide uses, for reference. --- # Glossary Definitions for the terms the rest of the guide uses. ## The session A **session** is one agent run: a single, continuous interaction with Claude Code, Codex, or pi, from the first message to the last. Each agent writes its session to a log file on disk as it works, and that file is what akari ingests. A session carries a stable **source id** (the agent's own identifier for the run), the **machine** it ran on, the account that owns it, the git branch and working directory it started in, timings, and its full transcript. A session still being written streams to the server as it grows, so the UI can show it updating live. Sessions are the unit you read, filter, price, and share. Everything else groups or describes them. ## Projects A **project** groups sessions by the code they were run against, keyed by **canonical git remote**. When the client resolves a session, it reads the working directory the session started in and asks git for that directory's `origin` remote. The result is normalized to a `host/owner/repo` key (for example `github.com/jssblck/akari`). Every worktree and every machine that clones the same repository shares that remote, so their sessions collapse into one project automatically. That is akari's core move: all the agent work on a repository in one place, wherever it happened. Not every session has a usable remote. The client classifies each into one of three kinds: - **Remote**: the working directory resolved to a canonical git remote. The project is that remote, shared across every machine and worktree. - **Standalone**: the directory exists but yields no usable remote (it is not a git repository, has no `origin`, has several origins, or an origin URL akari does not recognize). The session is still kept, keyed to a synthetic local project identifying the machine and folder. A live worktree records its main worktree root so the server can still fold sibling worktrees together. - **Orphaned**: the working directory is unknown or no longer exists on disk. The session is kept, keyed to its last-known local location. Standalone and orphaned projects are grouped and labeled apart from git-remote projects in the UI, so a folder that never had a remote does not masquerade as a shared project. In the web UI they reach you through the Sessions filter rail rather than the Projects index, which lists git-remote projects only. ## Machines and the fleet A **machine** is a computer running the akari client. One machine can push sessions from every agent installed on it; one server collects machines from a whole team. Every session records the machine it came from, so you can filter a project or the feed down to one workstation. The name defaults to the OS hostname but is configurable (the `machine` config key or the `AKARI_MACHINE` environment variable), so a fleet of ephemeral hosts can report under one stable identity instead of a distinct one-off hostname per run. See [the client](./the-client.md#machine-identity). The **fleet** is everything a server holds: all projects, all sessions, all machines. The Overview page reports fleet-wide, and the per-project analytics report the same figures narrowed to one project. A **trailing window** (7, 30, or 90 days, a year, or all of history) bounds those rollups; every figure on a panel respects the window you choose. ## The transcript A session's **transcript** is its parsed contents: an ordered list of **messages** (from the user, from the assistant, and system turns), each of which may carry **thinking** (the model's internal reasoning, when the agent records it), a **model** id, one or more **tool calls**, and **attachments** (images and other files that rode along with a message). A tool call records the tool's name, an optional category and file path, and its **input** and **result** bodies, each with a size, a media type, and a status. Some sessions spawn **subagents**: child sessions launched from within a parent run. akari links them to the session that spawned them, so a subagent shows up under its parent rather than floating loose in the feed. Tool bodies can be large (a single result might be megabytes of JSON or a base64-encoded image), so the transcript does not inline them. It holds a small reference, and the body itself lives in content-addressed storage (below), fetched only when you expand it. ## Tokens and cost Every turn records its token usage, split into classes akari tracks separately: - **Input** tokens fed to the model. - **Output** tokens the model generated. - **Cache write** tokens written to the model's prompt cache. - **Cache read** tokens served from that cache. The server prices each session from a **compiled-in rate table**: a mapping from model to per-token rates, built into the binary. Parsing a session looks up its model and multiplies each token class by its rate to get a dollar cost. There is no runtime pricing feed; updating rates means a new server build (which, because pricing is part of parsing, reprices old sessions automatically on the next [reparse](#parsing-and-reparse)). When a session uses a model the table does not know, its tokens are still recorded but that portion of the cost is left unpriced, and the session is marked **cost incomplete**. The UI shows such a total with a trailing `+` (for example `$1.42+`), so a partial figure is never silently rounded to look complete. Costs below a cent show extra precision rather than collapsing to `$0`. Per-session totals roll up across the session's turns; fleet and project totals roll those up further, always within the selected trailing window. ## Content-addressed storage Bulky tool bodies live in a **content-addressed store** (CAS): each body is keyed by the SHA-256 hash of its bytes and stored once, deduplicated across every session that references it. The transcript keeps only the hash and the body's size and media type; the UI shows a compact chip ("36 KB json") that fetches the real bytes on demand. Storing a body once no matter how many sessions repeat it keeps the database from ballooning on near-identical tool output. CAS bytes are served **per session**, never by bare hash. You can fetch a body only through a session that references it and that you are allowed to see, so the cross-session deduplication never leaks an internal body out through a public link. ## Parsing and reparse The server keeps two things for each session: the **raw bytes** it was ingested from, stored losslessly, and a **projection** parsed out of them (the messages, tool calls, usage, and cost the UI reads). The raw bytes are the source of truth; the projection is derived and disposable. That split is what makes **reparse** possible. A **reparse** replays stored raw bytes through the current parser and rebuilds the projection from scratch. It is how a parser or pricing improvement reaches sessions already ingested, with nothing re-uploaded. Reparse runs three ways: - **Automatically on startup.** The parser carries a compiled-in **epoch**; the server compares it against the epoch the stored data was last built under and, when they differ, reparses in the background while it keeps serving. There is no manual step after a parser upgrade. - **From the admin UI.** An admin can force a reparse from the Account page and watch its progress. - **From the CLI.** `akari-server reparse` forces one regardless of epoch, and `--agent ` limits it to one agent's sessions. While a reparse rebuilds, the parsed pages show a "reparse in progress" notice with a live progress bar rather than serving a half-rebuilt projection. Raw-byte reads (and content-addressed bodies) stay available throughout, since they are not part of the projection being rebuilt. [Self-hosting](./self-hosting.md#reparse) covers running one. ## Visibility A session's **visibility** is `internal` by default: visible to any signed-in user of the server. There is no private-to-one-user state; on a given server, signed in means you see everything. To share a session publicly, its owner **publishes** it, minting an unguessable public link at `/s/` that a logged-out viewer can open; unpublishing clears the link. A user can likewise publish their own **usage overview** at `/u/`. The full model, including who can delete what, is [Accounts and sharing](./accounts-and-sharing.md). --- Back to the [overview](./index.md) for the map, or the [akari repository](https://github.com/jssblck/akari) for the engineering design and code.