Skip to main content
Glama

ai-r

CI coverage tests License: MIT Python 3.11+

English | Русский | 中文 | 日本語 | Español

An agent reported "done." There's nothing to check it against.

ai-r reads the session history of any of the five coding agents and lets a fresh agent cold-check what git can't answer:

  • did it lie, did it break anything — did it keep its word, did it run anything dangerous (and roll it back if it did), what it actually changed, what it cost;

  • why it went that way — under which plan, with what intent, and whose hand was behind the edit.

Across our own corpus — 1600+ sessions of five agents in 20+ projects — that's how we found 312 risky commands (rm -rf, curl|sh, git push --force): the agent caught and rolled back two itself; the other 310 ran silently — git won't show them.

git shows what made it into the code; ai-r shows whether you can trust how the agent got there. Read-only: no LLM calls, no network.

Quick example — an agent asks about history

The primary mode is MCP: an agent (Claude, Codex, …) calls ai-r directly and asks about history in plain language. For example — pull the plan the previous agent settled on, drafts discarded:

Show me the plan from the last session — final only, no intermediate revisions.
→ plan(session=…, kind="final")  →  get_body(id, shallow=true)

  plan:            "Migrate auth to JWT: 1) extract the check…"
  dropped_drafts:  2   ← two drafts the agent threw away along the way
  session:         a3f… (claude)

Fast edit attribution — one terminal command, across every agent at once:

ai-r find-file-edits auth.py --since 2026-06-01
2026-06-03  codex   auth.py  "add a refresh token"                 edit
2026-06-07  claude  auth.py  "extract the check into middleware"   edit

Related MCP server: cli-history-hub

What hurts

  • "Done, I did X per plan Y" — with nothing to check it against: the agent keeps the plan in one shape, the edits in another.

  • You switched agents mid-task and lost the thread. There's nowhere to ask "what did the other agent already try?"

  • An edit shows up in a file — and it's unclear which agent made it, and on what request.

One cause: every agent writes its history its own way — Claude and Codex in JSONL, OpenCode in SQLite, Antigravity in "brain" directories, Pi in per-project JSONL. Five formats, five layouts — together they don't reconcile.

The promise

ai-r folds all five into one read-only interface. Point any agent — or a script, or yourself — at any session, no matter which tool recorded it. One query shape per agent; format differences are normalized inside the parsers.

Even with a single agent it works: you audit your own Claude history (or Codex…). The five formats are so your history doesn't break when you switch tools — not a requirement to have all five.

As a source for RAG

In an "LLM + external data source" setup (RAG), ai-r is the source — more precisely, a retrieval layer over agent sessions. For a query it returns not a slice of log but parsed entities: the plan, the intent, the authorship of an edit — with a reference to the body the agent can pull if it needs it.

It doesn't replace your RAG over code and docs; it adds a source the others can't reach. The usual sources you retrieve from: documentation, commit history, Stack Overflow, internal wikis, code bases, bug reports. Agent sessions aren't on that list — even though only there is it recorded why an edit happened at all.

Retrieval is BM25 (ranked keyword search), with optional semantic re-ranking. No vector database, no second LLM: all local, results reproducible. BM25 here isn't a shortcut — GitHub lists it alongside vector retrievers: "Common retrievers include sparse methods like BM25 and dense vector retrievers using neural networks."

Key features

Each item is a trust question from the first screen and the verb that answers it:

  • Did it keep its word — plan vs. reality. Pulls the final plan (separate from the discarded dropped_drafts) and checks it against what actually made it into the edits — catching "did X per plan Y" where Y is no longer that plan. (plan, session_diff)

  • Did it run anything dangerous — and roll it back. Flags risky commands (rm -rf, curl|sh, git push --force) and, from the turns that follow, sees whether the agent caught it and rolled back — or it passed silently. (incidents, query tool_kind=bash)

  • What it actually changed, and by whose hand. Any edit or call → the agent that made it, plus the request that triggered it; including edits made through the shell (> file under codex) that a plain diff misses. (find-file-edits, find-tool-calls)

  • What it cost. Tokens and cost per session — exact where the format recorded the usage, an honest estimate where it didn't, never invented. (session_stats with_tokens, aggregate group_by=model)

  • Why it went that way. The intent behind an edit (the request before it), under which plan, on which model — "why", not just "what". (query with_intent)

  • Small answer, body on demand. A record carries a reference to the content (hash + length); the full text comes as a separate request. A reader, not a guard: read-only, it runs nothing and writes nothing to an agent's history.

How ai-r knows

Deterministically, with no second LLM guessing — and honest about the edges:

  • dangerous command — a pattern over the call string (rm -rf, curl|sh, git push --force, …). Anything obfuscated (exec(input())) the pattern won't catch — that's a declared boundary, not a silent miss.

  • rollback — marked "confirmed" ONLY when a regret/apology marker from the agent sits nearby (within the window of following turns; the marker itself is a bilingual ru/en pattern, not an LLM sentiment call). No marker → it stays an unconfirmed candidate: ai-r won't infer a silent rollback, it honestly says "not confirmed".

  • lied about the plan — ai-r doesn't decide for you. It lays the plan entity next to the session's reconstructed edits (session_diff) — the mismatch is visible to you or a reviewing agent. That's evidence assembly, not a semantic verdict.

Zero LLM calls, read-only — the numbers are reproducible and "confirmed" is never guessed.

What you use it for

  • Audit sessions with a fresh pair of eyes. A new agent with an empty context coldly checks past sessions on three axes: were promises and requirements met; are the decisions sound and well-judged; how deeply was the question explored — what the agent missed. This catches agents that finished the task but misled on the planning — something a live chat hides, and that steers you into wrong decisions.

  • Continue past a spent context — without losing detail. /compact erases the specifics. Instead, open a fresh session: it reads the previous session's logs and continues from its conclusions, without re-burning context on what's already been worked out. The original session stays intact — for audit and search. The new session can run in any agent: the history reconciles regardless of the tool.

  • Feeds your memory system. Keeping memory and summaries à la Karpathy, or your own method? ai-r gives you, for AI chats, what you already do with message history — parsed entities to build a lasting memory of the details that matter.

  • Recall what you did and why. Why was this file edited? Why was this rule added? Find the session where the file changed and read the request before the edit.

How it differs from session-search tools

A handful of cross-agent tools now read more than one agent's history (jazzyalex/agent-sessions, Dicklesworthstone/coding_agent_session_search, hacktivist123/agent-session-resume). Almost all are about search and timeline: find a session, scroll the history.

ai-r goes deeper: it extracts the plan, intent, and authorship as ready-made entities you build memory on. Search finds text — ai-r answers why. Technically a search tool could also dig a plan out of a session's text, but it doesn't hand it back parsed into a single, normalized shape — with ai-r that's the primary surface.

Capability

Single-agent viewers

Cross-agent search tools

ai-r

Reads >1 agent's logs

No

Yes

Yes — Claude, Codex, OpenCode, Antigravity, Pi

Programmatic surface

Mostly GUI/TUI

Mostly TUI/CLI/app

MCP + CLI + Python SDK

Attribution (edit/command → agent + intent)

—

Partial

Yes — find-file-edits / find-tool-calls

Audit replay (reconstruct a session's changes, no git)

—

Rarely

Yes — session_diff

Plan extraction (final vs draft, normalized)

—

—

Yes — plan

Scope

Viewer

Search / resume / memory

Read-only extraction core

Competitor columns reflect their public docs as of 2026-07; where a capability is unclear we under-state rather than over-claim.

We deliberately don't compete on agent breadth, speed, or TUI richness. ai-r's wedge is extracting the "why" and structured entities for machine consumption.

Proven in practice

ai-r already reads its own development history — across all five agents. Real tools run on it (they live separately, on top of its read-only API):

  • auditor — a fresh agent coldly checks what the previous one actually did and decided. This caught agents that quietly fibbed about the plan.

  • summarizer (export rounds) — renders a session into a ready handoff doc.

  • ai-local-reader — a read-only skill: audits past sessions from disk across all agents.

These tools are workflow-side, outside this repo. ai-r itself only reads and returns data.

Supported agents

Agent

Storage

Parser

Claude Code

~/.claude/projects/

JSONL

Codex

~/.codex/sessions/

JSONL

OpenCode

~/.local/share/opencode/opencode.db

SQLite (snap/flatpak auto-detect)

Antigravity

~/.gemini/antigravity/brain/

JSON / markdown brain directories

Pi

~/.pi/agent/sessions/<encoded-cwd>/*.jsonl

JSONL

Not your agent? Adding a sixth is one parser module; the read-only pattern ports to any tool in minutes. See CONTRIBUTING.md.

Surfaces

ai-r gives the same reading power three ways:

  • MCP server (ai-r-mcp) — 15 tools over JSON-RPC, so any MCP agent calls it directly (recommended). Default is stdio; optionally a shared http server (one warm process for all agents instead of a per-agent stdio swarm), see the http extra under Quick start. Registration — see docs/mcp-registration.md.

  • CLI (ai-r) — subcommands for scripts and manual use (list / read / search / find-file-edits / find-tool-calls / file-frequency / detect-agent / export rounds). Search operators — docs/search-operators.md.

  • Python SDK (from ai_r.parsers import ...) — parsers, typed Session/message models, and the event verbs, to build your own tools.

Method vocabulary

The full dictionary of public verbs and presets (signatures, parameters, behaviour) lives in its own file: docs/methods.md.

Event core

The verbs above are new: one event core replaces a pile of one-off tools. Each parser reads one agent's logs and emits typed models, normalized into a single agent-neutral stream — user_turn / assistant_turn / tool_call(...) / plan_event. A small set of verbs filters, aggregates, and diffs that stream; agent differences (ExitPlanMode vs update_plan vs implementation_plan.md) stay hidden inside the parsers — the caller sees one shape.

An honest boundary: this is extraction of entities only — turns, tool calls, plans, intents, reactions. It is not a graph and not a memory store. What you do next (knowledge graph, Obsidian, persistent memory) is on your side, outside this repo. For the full layering and the MCP tool list, see docs/architecture.md.

Quick start

Try it without installing — if you have uv:

uvx --from agent-session-reader ai-r list          # CLI: list sessions
uvx --from agent-session-reader ai-r-mcp           # MCP server (stdio)

Nothing lands on your system: uvx downloads the package into a temporary cache and runs it. Good for looking at your sessions right now, or for wiring ai-r-mcp into an agent's MCP config by hand.

Full install (1 command) — also patches your configs:

Requirements: Python 3.11+ with venv or pip, and jq (used to auto-patch the Claude and Antigravity MCP configs — the others don't need jq).

git clone https://github.com/pro-target/ai-r.git ~/dev/ai-r
cd ~/dev/ai-r && bash install.sh

The installer creates a venv, installs the runtime package, patches MCP configs for Claude, Codex, OpenCode, Antigravity (where the configs exist), installs the Pi CLI skill, and runs smoke tests. That auto-patch is exactly what uvx doesn't do — there you edit the configs yourself.

Optional extra — tokens: AI_R_EXTRAS=tokens bash install.sh (or pip install "ai-r[tokens]") adds tiktoken for better token estimates on sessions whose format stores no exact usage numbers. Fully optional: without it exact numbers still come straight from the session files where recorded, and the fallback estimate degrades to a rough chars/4 heuristic, honestly labeled estimate — never a crash.

Optional extra — semantic: AI_R_EXTRAS=semantic bash install.sh (or pip install "ai-r[semantic]" + a one-time model download the installer does for you) enables sort="semantic" on text search (query, search_sessions) — the BM25 top-50 candidates are re-ranked by meaning.

  • Model. A local multilingual embedding model, intfloat/multilingual-e5-small (int8 ONNX, ~118 MB, MIT), run directly via onnxruntime + tokenizers + numpy, no torch, no persistent index. Chosen for strong cross-lingual retrieval (a Russian query finds an English session and vice versa) at a small size.

  • How the score works. BM25 picks the 50 best word-matches (a cost budget, not a quality cut-off — there is deliberately no similarity threshold, because this model family scores even unrelated texts ≈0.7). Within that pool the final score is 75 % meaning + 25 % word match — meaning dominates, while the word share keeps exact-term hits from being drowned and breaks ties.

  • Fail-soft. Without the packages or model files, sort="semantic" honestly falls back to the BM25 order and the response says why (semantic: {active: false, reason, fallback: "bm25"}) — never a crash.

Two knobs keep the model well-behaved inside a long-lived MCP process (both env-tunable, both degrading to the default on blank/invalid input — never a crash): AI_R_SEMANTIC_THREADS caps how many CPU threads onnxruntime may use per inference (default 2, never more than the machine's core count — so it does not grab every core and fight the server for CPU), and AI_R_SEMANTIC_IDLE_SEC frees the loaded model's ~118 MB of RAM after that many idle seconds (default 300); the next request transparently re-loads it.

Optional extra — http: AI_R_EXTRAS=http bash install.sh (or pip install "ai-r[http]") adds uvicorn and enables a shared streamable-http transport (requires mcp>=2).

  • Why. By default every agent spawns its own ai-r-mcp over stdio — under multi-agent fan-out that is N processes, each with a cold cache, re-scanning the corpus (the measured cause of RAM exhaustion). With AI_R_MCP_TRANSPORT=http a single warm server on localhost (default 127.0.0.1:8756) is shared by every agent instead of a swarm; the systemd units in packaging/systemd/ add socket-activation with idle self-exit.

  • Security (fail-closed). The bind is loopback-only. Browser-based attacks (DNS rebinding) are cut off by the SDK's Origin/Host allowlist (always on for loopback). Remote access requires AI_R_MCP_ALLOW_REMOTE=1 and an AI_R_HTTP_TOKEN — without the token it refuses to start (transcripts carry secrets). On loopback the token is optional (protection against another local user on a shared box); the client sends an Authorization: Bearer <token> header.

  • Knobs (env):

    • AI_R_MCP_PORT — port (default 8756).

    • AI_R_MCP_IDLE_SEC — idle self-exit threshold.

    • AI_R_MCP_HOST / AI_R_MCP_ALLOW_REMOTE — bind host / allow non-loopback.

    • AI_R_HTTP_TOKEN — bearer token (required for a remote bind).

    • AI_R_MCP_STATELESS — stateless request dispatch (default 1): the long-lived shared server outlives its clients, so a request with a stale Mcp-Session-Id is served instead of being rejected with 404 "Session not found"; 0 restores the stateful registry.

    • AI_R_HAYSTACK_CACHE_MAX — search cache ceiling by entry count.

    • AI_R_HAYSTACK_CACHE_CHARS_MAX — by total size (an RSS safeguard for a long-lived server).

    • AI_R_MSG_CACHE_MAX / AI_R_MSG_CACHE_BYTES_MAX — the core read cache (one corpus scan + one transcript parse per unchanged session, shared by query / get_body / audit_brief / the plan projections): ceiling by entry count (default 8) and by summed source bytes (default 64 MiB).

Heavy sessions (over ~1 MB): prefer the CLI, or single MCP calls. A multi-megabyte transcript is full-parse work; a cold MCP call on one — worse, a parallel batch of them — can outlast the client's request timeout. For those sessions use the CLI (ai-r audit-brief <uuid>, ai-r read <uuid>) or sequential single MCP calls; the read caches make warm repeats cheap, but the first touch of an unchanged session still costs one honest scan.

Both extras are fully optional: without them stdio mode and the BM25 order work as before.

Boundaries: a reader, not a guard

  • Read-only. It never runs an agent's code and never writes to its history — it reads and returns.

  • No graph, no memory. It extracts entities (turns, calls, plans, intents). Building a knowledge graph or memory out of them is your job, not its.

  • Not an access-control layer — except the http transport. Anyone who can reach the CLI, MCP over stdio, or the package reads any session: it's the same local user, so an authorization check in front of the parsers would guard nothing. The exception is the shared http transport: it's reachable over a socket, so it carries an Origin allowlist and an optional bearer token (required for a remote bind, see the http extra above). Either way, keep the data where untrusted local processes can't reach.

  • Session content is data, not commands. Whoever reads (auditor, summarizer) must treat session text as data, not instructions. See Security.

Acceptance (end-to-end scenarios)

The public surface is covered by end-to-end scenarios an LLM agent runs against the live MCP (complementing pytest). Full list — docs/scenarios.md.

Example: ai-r in action

A gallery of real examples — one per capability (error analysis, dangerous commands, network trail, token burn, plan comments, commit phantom-check, cross-agent file history, cross-lingual search, zombie subagents, git-less diff): docs/examples/showcase-gallery.md.

Next — documentation

Development

git clone https://github.com/pro-target/ai-r.git
cd ai-r
pip install -e ".[dev]"
pytest --cov=src/ai_r
  • 1300+ tests, CI requires ≥85% coverage

  • Versioning: SemVer; while on 0.x, a minor release may break compatibility — where possible a migration path is given (a loud deprecation warning before removal); changes land in CHANGELOG.md

  • Conventional Commits (feat:, fix:, docs:, …)

  • On adding new agents, see CONTRIBUTING.md and docs/parsers.md

claude code session reader · claude code session parser · codex session parser · opencode session reader · antigravity brain parser · pi agent session reader · zcode session reader · zcode cli session parser · rag over agent sessions · bm25 retriever · retrieval layer for ai agents · grounding · mcp server · structured context · cross-agent attribution · ai coding agent audit · ai agent session history · mcp session tools · read-only session reader · agent session replay · resume agent session · agent handoff · plan extraction · tool-call audit · file edit attribution · multi-agent coding · claude codex opencode antigravity pi zcode

License

MIT — see LICENSE.


Get started: uvx --from agent-session-reader ai-r list — see your sessions right now; or clone + bash install.sh for the full install with MCP-config auto-patching (docs/mcp-registration.md). One read-only surface over every agent's history.

Available Tools

18 tools
aggregateA

Roll a list of row dicts up by group_by — the generic stats verb.

Reproduces session_stats (group_by ∈ agent/dir/date/ kind over a session inventory) and file_frequency (group_by="file" over a find_file_edits record stream) as a pure fold over already-materialized rows — no re-parsing. session_stats is now a thin preset over this verb (rank_by="stats" + kind_split).

Args: rows: The row dicts to fold (query output, find_file_edits records, or a session inventory). group_by: The bucket key — a row field name (agent / dir / date / kind / file / model — query rows carry the producing model where the format records one / …). Missing/empty values bucket under "(unknown)". metrics: Which numbers each bucket carries. One or more of count / sessions / edits / intents / agents / messages / files / tokens / component_tokens. Defaults to ["count"]. tokens (F3.3) folds per-row tokens blocks (the shape session_stats(with_tokens=True) rows carry, or a bare int total) into {input, output, reasoning, cache_read, cache_write, total, exact, estimated, unknown} — sums over rows that carry each field (null when none does) plus honest provenance counters (exact + estimated + unknown == len(rows)). component_tokens (F3.3) folds per-row component_tokens blocks (the shape :func:ai_r.tokens.component_tokens produces, as read_session(with_tokens=True) attaches) into summed event-taxonomy components (user_turn / assistant_turn / thinking / plan and a tool_call per-kind sub-dict) + total + provenance counters (estimated / unknown; never exact — always an estimate). A component/kind no row carried stays absent (never a fabricated 0). rank_by: Group ordering — "default" (edits→sessions→count→label, the file_frequency order) or "stats" (sessions→edits→label, the session_stats order). kind_split: When True, add the session_stats RISK-4 fields (kind_split_available + a degenerate-split note).

Returns: {"group_by", "groups": [...], "totals": {...}} (plus kind_split_available/note when kind_split) or the standard {"error": ..., "message": ...} dict on an unknown metric/rank_by.

NameRequiredDescriptionDefault
rowsYes
metricsNo
rank_byNodefault
group_byYes
kind_splitNo

Output Schema

NameRequiredDescription

No output parameters

audit_briefA

Token-lean, budgeted session digest for auditors — the audit_brief preset.

One call answers "what happened in this session, verbatim where it matters", inside a hard character budget. A preset over the existing core, not a second engine (project preset rule): ONE query scan over the session's events supplies the user turns (VERBATIM — the auditor's ground truth) and the tool/file footprint (folded by aggregate(group_by="tool_kind") + the edit/write rows' existing file refs); the plan/plan_feedback projections supply the decision trail; :mod:ai_r.tokens (the same SSOT behind session_stats(with_tokens) / read_session(with_tokens)) supplies the token breakdown.

Deterministic budget algorithm: build the full digest, then tighten in a FIXED ladder until the serialized JSON fits budget_chars (default 15000; 0 = unlimited) — (1) drop tool-call error details, (2) drop the per-file edit list, (3) drop plan bodies + feedback quote/comment texts (counts/references always stay; bodies on-demand via get_body). User turns are NEVER truncated: if they alone exceed the budget the response carries budget.over_budget: true + a note naming the full projections — never a silently clipped ground truth.

session accepts the full uuid or a unique id prefix (e.g. the 8-hex head), resolved through the SAME id-prefix matching locate uses — the digest's session.uuid echoes the full resolved id; an ambiguous prefix is invalid_argument naming the candidates (capped), zero matches is not_found with closest-title suggestions. agent is an optional hint (None = the id resolves across every parser, like read_session). redact=true (default) masks secrets in the emitted title / user texts / plan bodies / feedback pairs. The response is section-structured (session / user_turns / plans / tools / files / tokens / component_tokens / budget); the CLI mirror is ai-r audit-brief <uuid> (markdown, --json for this dict).

Thin wrapper over :func:ai_r.audit_brief.audit_brief: a ValueError becomes {"error": "invalid_argument"}, an unknown session id {"error": "not_found"}.

NameRequiredDescriptionDefault
agentNo
redactNo
sessionYes
budget_charsNo

Output Schema

NameRequiredDescription

No output parameters

detect_currentA

Return the current runtime identity (session + agent) from env/fs.

NOT a session-query — this reads the runtime environment (env vars + per-session flag files), reusing the exact cascade behind the ai-r detect-agent / ai-r detect-session CLI subcommands.

Args: agent: Optional hint (accepted for symmetry with the CLI's deprecated --agent flag); the cascade scans all agents.

Returns: {"session_id", "agent", "model", "resume_command", "candidates": [...], "verified", "self"} where session_id / agent describe the highest-priority candidate, model is the current session's model — the LAST assistant model recorded in its transcript (null when identity is incomplete or the format records no model signal — never guessed) — resume_command is the ready-to-run shell one-liner that reopens the detected session in its agent's CLI (F2.2, text only, never executed; null when no real command exists) — and candidates is the full cascade for disambiguation. Returns {"error": ..., "message": ...} on an unknown agent hint.

NameRequiredDescriptionDefault
agentNo

Output Schema

NameRequiredDescription

No output parameters

diffA

Stitch edit rows into a per-file chronological diff — the diff verb.

Reproduces the synthesis of session_diff: given the edit events of a session (query(type="tool_call(edit)", session=…) — plus write / shell-redirect events), group them per file in chronological order and render a stitched, readable diff. Bodies are fetched on demand (via each event's stored message_index), never inlined on the row.

Args: rows: Edit-event dicts (query output). Each must carry an id and a refs list with a file entry; unresolvable rows skip. per_file: Group by file (the only mode today). format: "unified" (the only rendering today). redact: When True (default) secrets in the stitched output are masked as [REDACTED_<TYPE>] and the result carries a redactions type→count dict when any replacement happened; False returns raw content.

Returns: {"files": [{"file", "edits", "diff", "hunks"}], "count", "caveats"} (same shape + caveats as session_diff, size-bounded the same way: capped fields named in the per-file truncated_fields, byte budget → output_truncated) or the standard {"error": ..., "message": ...} dict on an unsupported format.

NameRequiredDescriptionDefault
rowsYes
formatNounified
redactNo
per_fileNo

Output Schema

NameRequiredDescription

No output parameters

find_file_editsA

Find every file edit across sessions, cross-agent by default.

redact=True (default) masks secrets in emitted record fields as [REDACTED_<TYPE>] and adds a redactions type→count dict when any replacement happened; redact=False returns raw content.

Reference-by-default: to keep an audit listing small, each record does not inline the full edit body. Instead it carries a light-weight reference — input_sha256 (hash of the body) and input_chars (its length) — so you can see a body exists and how big it is. Fetch the body on demand with get_body / read_session (keyed by session_uuid

  • message_index). Pass include_input=True to inline the full body under input instead.

Size-bounded output: over-long intent / assistant fields are cut with a …[truncated] marker (named in the per-record truncated_fields) and emission stops at a total byte budget (output_truncated — distinct from the count-based truncated).

Default time window: a call with NO narrowing filter at all (no agent / since / until) is scoped to the last 7 days instead of the whole corpus; the response then carries default_since (the applied bound) plus a note saying so. Any explicit scope disables the default — pass e.g. since="1970-01-01" to deliberately scan the full history.

Thin wrapper over :func:ai_r.find_file_edits.find_file_edits that translates the core ValueError contract into the {"error": "invalid_argument", "message": str(exc)} shape the MCP client expects.

NameRequiredDescriptionDefault
pathYes
agentNo
limitNo
sinceNo
untilNo
redactNo
include_inputNo

Output Schema

NameRequiredDescription

No output parameters

find_tool_callsA

Find every tool call across sessions, cross-agent by default.

redact=True (default) masks secrets in emitted record fields as [REDACTED_<TYPE>] and adds a redactions type→count dict when any replacement happened; redact=False returns raw content. Filters always match the RAW, pre-redaction text.

session scopes the scan to a single session uuid (or a list of uuids) — same semantics as the query facet. None = every session; a wide since/until with NO session therefore surfaces calls from unrelated sessions, so pin it when auditing one conversation.

Exactly one of tool_name (exact, case-insensitive) or tool_name_pattern (substring, case-insensitive) must be set.

Optional filters combine with AND: input_contains / output_contains (case-insensitive substring on the full, pre-cap input/output), output_excludes (drop records whose output contains it) and is_error (tri-state: None all, True failures only, False successes only). output_mode selects output truncation — "head"/"tail"/"smart"; None is adaptive ("smart" on errors, "head" otherwise). Each record also carries is_error_reliable (True only for Claude/OpenCode) plus the wrapper-aware classification: tool_kind (edit/write/read/bash/task/skill/mcp/ web/other) and tool_resolved — the real name under a Skill/Task/MCP wrapper (subagent type, skill name, or "<server>:<tool>"); None when there is no wrapper or the input carries no name signal.

A record whose call has a correlated result also carries tool_use_id — the join key back to a spawned subagent's own session (the child stores it as extra.spawn_tool_use_id). On a spawn (tool_kind="task") it additionally carries subagent: what the child COST — model (the model it actually resolved to, which may be a cheaper pinned tier than the parent's), agent_type (persona), tokens (EXACT billed usage, source="exact", full token-block shape), status, duration_ms, tool_uses. Honest gaps: a background spawn (status="async_launched", sidecar written before the run exists) reports its model with no tokens key — never a fabricated zero; its real cost and persona come from read_session(include_subagents=True) → subagent_rollup.children. A record carrying several tool results drops the sidecar rather than billing it to the wrong subagent.

with_subagent_cost=True (opt-in) recovers exactly that for the spawn records here: each subagent sidecar is JOINED to the spawned child's own files, adding the persona (agent_type) from the child's agent-*.meta.json, the models it ran on, its EXACT billed tokens (source="exact", an estimate is never merged into the billing field) and child_uuid. So a background spawn — anonymous and price-less in the launch-time sidecar — becomes a named, priced row. The child is preferred over the sidecar, which stays the fallback for a child that cannot be joined (not yet on disk, meta corrupt): its tokens are then left absent, never zeroed. Default False reads no per-spawn child file (a cross-corpus scan does not pay the join).

Thin wrapper over :func:ai_r.find_tool_calls.find_tool_calls that translates the core ValueError contract into the {"error": "invalid_argument", "message": str(exc)} shape the MCP client expects.

NameRequiredDescriptionDefault
agentNo
limitNo
sinceNo
untilNo
redactNo
sessionNo
is_errorNo
tool_nameNo
output_modeNo
input_containsNo
output_containsNo
output_excludesNo
tool_name_patternNo
with_subagent_costNo

Output Schema

NameRequiredDescription

No output parameters

get_bodyA

Return the on-demand body for an event / plan id.

For a plan_event id: the full plan text and/or Codex steps (bodies are deliberately kept off the event stream so callers pay for them only when needed). For a user_turn / assistant_turn id: the turn text.

shallow=True (plans only) returns just the final plan of the id's task, dropping the bodies of superseded draft revisions — the S6 case where a subagent receives one plan without the draft noise (dropped_drafts lists the ids that were elided).

max_chars bounds the returned body/text (default 500_000, generous enough that ordinary bodies are never cut; pass 0 to disable). When it trips, the field is sliced with a …[truncated] marker and body_truncated: true is set.

redact=True (default) masks secrets in the emitted text/body/title/steps as [REDACTED_<TYPE>] and adds a redactions type→count dict when any replacement happened; redact=False returns the raw content.

include_thinking=True (turn ids only): attach the model's reasoning (message.thinking) as a separate thinking field on the returned body, re-read from the hosting message. Default False leaves the body byte-identical to the historical shape (no thinking key). Fail-soft: a turn without reasoning never carries the key.

Returns the body dict, or {"error": ..., "message": ...} on a bad id.

NameRequiredDescriptionDefault
idYes
redactNo
shallowNo
max_charsNo
include_thinkingNo

Output Schema

NameRequiredDescription

No output parameters

incidentsA

Dangerous shell commands + regret reactions — the incidents preset.

One call answers "where did an agent run something destructive — and did it then apologise?". A preset over the existing core, not a second engine: ONE query scan (type="tool_call", tool_kind="bash") supplies the candidates, a deterministic danger dictionary (harvested from public agent-guardrail rule sets, calibrated on real history) selects the dangerous commands, and a bilingual (ru + en) regret dictionary scans the next reaction_window messages (default 6) for an apology/rollback reaction — the two-step check behind the confirmed flag. Zero LLM, zero guessing: no dictionary hit → no incident; no reaction → confirmed: false, never inferred.

Filters (all parameters): agent, session (uuid or list of uuids), since/until (ISO bounds on the call ts), category (fs/git/db/net — unknown values fail loud), confirmed (include default | only | exclude), noise and project_dir (session-level, same semantics as query).

Each incident record carries the query event id (walk its context via query(relative_to=...) / read_session), the matched patterns + categories, a char-capped command fragment centred on the hit (token budget — full context stays on-demand), is_error (null when the agent's format has no correlated outcome signal — honest, cross-agent), confirmed and reaction (message_index/offset/role/marker labels/capped preview; null when unconfirmed). count/confirmed_count/ by_pattern always reflect the FULL match set; limit (default 50, 0 = no cap) bounds only the emitted records (truncated).

Dictionary caveat (documented, not hidden): patterns are a deterministic dictionary, not a shell interpreter — a command that merely mentions a dangerous string (e.g. echo "rm -rf /") can still match. Matching runs on the extracted command field (a Bash description alone never fires) and always on the RAW stored text; redact=true (default) masks secrets only in the emitted session_title/command/reaction.preview fields (redactions type→count dict when anything was masked). When count == 0 the response carries diagnostics so an empty result is explainable (missing source dir vs all-excluding filter vs a genuinely clean history).

Thin wrapper over :func:ai_r.incidents.incidents that translates the core ValueError contract into the {"error": "invalid_argument", "message": str(exc)} shape the MCP client expects.

NameRequiredDescriptionDefault
agentNo
limitNo
noiseNoinclude
sinceNo
untilNo
redactNo
sessionNo
categoryNo
confirmedNoinclude
project_dirNo
reaction_windowNo

Output Schema

NameRequiredDescription

No output parameters

list_sessionsA

List discoverable sessions, optionally filtered by agent.

Results are sorted by date (newest first) and paginated with limit/offset so the payload stays small. The default limit guards against dumping an unbounded number of sessions.

Each summary carries kind ("agent" for a top-level session, "subagent" for a spawned subagent/sidechain) and parent_uuid (the parent session's uuid for subagents, else None). Subagent detection covers Claude, OpenCode, Codex and Pi; Antigravity's format has no parent signal, so it always reports kind="agent".

Each summary also carries the F1.4 origin fields, None when the source format has no signal (never fabricated):

  • project_dir — the project directory the session ran in (Claude: transcript cwd / Desktop metadata / verified slug decode; Codex: session_meta.cwd; OpenCode: session.directory; Pi: header cwd; Antigravity: no signal).

  • launch_surface — where the session was driven from (Claude: "claude-cli" | "claude-desktop"; Codex: the raw originator, e.g. "codex_vscode"; Antigravity: "antigravity-ide" | "antigravity-cli"; OpenCode/Pi: no signal).

Each summary also carries models — the unique model ids observed in the session, in order of first appearance (Claude: assistant message.model; Codex: turn_context.model; OpenCode: message.data.modelID; Pi: assistant message.model; Antigravity records no model signal). [] when the format carries no signal — honest absence, never fabricated.

Each summary also carries the A3 recency signal, measured against a single wall-clock now sampled once for the whole call:

  • last_activity — the last-activity timestamp as an explicit ISO string (same instant as date; date is kept for backward compatibility);

  • age_sec — whole seconds since last_activity (clamped at 0 when a future timestamp implies writer/reader clock skew);

  • activity — "fresh" if age_sec is at or under the AI_R_STALL_SEC threshold (default 600 s = 10 min), "stale" if past it.

Honest contract (F1.1): activity describes only the recency of the last written record. It is not a claim about process liveness — a session file cannot show whether its producer is still running. "Running but silent" vs. "crashed" is a consumer-side inference (correlate activity == "stale" with an OS pid-alive check); ai-r does not fabricate it.

Args: agent: One of claude, codex, opencode, antigravity, pi. When omitted, every supported agent is queried. limit: Max sessions in this page. 0 means no cap (use with care: may return a very large payload). Defaults to 100. offset: Zero-based index of the first session to return. Use with limit to page through total. kind: Optional filter. "agent" returns only top-level sessions, "subagent" returns only subagent sessions. When omitted (default), both kinds are returned. noise: Noise filter — a session is noise when it is a spawned subagent (kind == "subagent" or parent_uuid set). "include" (default) returns everything, "exclude" drops noise sessions, "only" returns only noise sessions. kind and noise compose (AND). project_dir: Keep only sessions whose project_dir equals this path or is a descendant of it (path-boundary aware: /a/b matches /a/b and /a/b/sub, never /a/bc); trailing slashes ignored. Sessions without a project_dir signal never match. Composes with the other filters (AND). redact: When True (default) secrets in emitted title / extra values are masked as [REDACTED_<TYPE>] and the response carries a redactions type→count dict when any replacement happened; False returns raw titles.

Returns: {"sessions": [...], "total": int, "offset": int, "limit": int, "truncated": bool}. total is the full count matching the agent (and kind/noise) filter; truncated is True when more sessions remain beyond this page. When total == 0 the dict additionally carries diagnostics (scanned agents + session counts, source-dir presence, cause hints) so an empty inventory is explainable.

NameRequiredDescriptionDefault
kindNo
agentNo
limitNo
noiseNoinclude
offsetNo
redactNo
project_dirNo

Output Schema

NameRequiredDescription

No output parameters

locateA

Find a session across all agents by uuid / id-prefix / title — locate.

One call answers "I remember a session — where does it live and how do I read it?". needle is a full uuid, an id prefix (e.g. the 8-hex head), or a case-insensitive title substring. A thin preset over the existing per-parser inventory (the same list_sessions walk — zero new scanning code) with a deterministic algorithm inside: prefix-match on uuid/path-stem OR substring-match on title, ranked by last activity (mtime) descending. Each match carries where it lives (path / agent / project_dir / date / size_bytes), the honest local-content claim readable (false for a reference-only stub whose transcript is not on this machine), and the ready-to-run commands: read_command (ai-r read <uuid> --agent <agent>) + resume_command (F2.2 — text only, never executed).

limit bounds the emitted list (0 = no cap; count keeps the full total, truncated flags the cut). Zero matches → an honest empty with closest-title suggestions + diagnostics — never a fabricated match.

web=true (v1, honest scope) adds web sessions KNOWN LOCALLY only: materialized hook-export files ($SW_HOME/web-sessions, default ~/.session-watch/web-sessions) and ~/.claude.json → projects[*].lastSessionId teleport stubs (id known, transcript NOT local — content_local: false). The fuller per-repo teleport-picker sweep needs a PTY and is a documented follow-up, not guessed here.

Thin wrapper over :func:ai_r.locate.locate (ValueError → {"error": "invalid_argument"}).

NameRequiredDescriptionDefault
webNo
agentNo
limitNo
needleYes
redactNo

Output Schema

NameRequiredDescription

No output parameters

networkA

Network-egress audit — the network preset (F4.3).

One call answers "where did an agent reach out to the network — and how risky did those requests look?". A preset over the existing core, not a second engine: ONE query scan (type="tool_call", tool_kind="web") supplies the candidates — Claude WebFetch/WebSearch, OpenCode webfetch, Codex web_search (surfaced from web_search_call rollout records), Gemini/Antigravity web_fetch/google_web_search; Pi records no web tool (honest absence). The request target (url/query) is extracted from each call's own input and assessed with a deterministic risk dictionary: plain_http, credentials_in_url, secret_in_url / secret_in_query (the redaction patterns double as the detector), ip_literal_host, private_or_local_host, punycode_host. Zero LLM, zero guessing: no extractable target → honest null fields; a risk fires only on parse/regex evidence.

Filters (all parameters): agent, session (uuid or list of uuids), since/until (ISO bounds on the call ts), kind (fetch|search — derived from the extracted fields, unknown values fail loud), risk (include default | only | exclude), domain (host equals-or-subdomain match), noise and project_dir (session-level, same semantics as query).

Each request record carries the query event id (walk its context via query(relative_to=...) / read_session), the derived kind, char-capped url/query (token budget — full context stays on-demand), domain, the risks labels and tri-state is_error (null when the agent's format has no correlated outcome signal — honest, cross-agent). count/risky_count/ by_domain/by_risk always reflect the FULL match set; limit (default 50, 0 = no cap) bounds only the emitted records (truncated).

Honesty caveats (documented, not hidden): risk labels are a deterministic dictionary, not a threat oracle; MCP-mediated network access (browser-automation servers etc.) stays under tool_kind="mcp" — a name alone cannot prove an MCP server touches the network, so it is never guessed into this audit. Risk assessment runs on the RAW stored strings; redact=true (default) masks secrets only in the emitted url/query/session_title fields (redactions type→count dict when anything was masked). When count == 0 the response carries diagnostics so an empty result is explainable.

Thin wrapper over :func:ai_r.network.network that translates the core ValueError contract into the {"error": "invalid_argument", "message": str(exc)} shape the MCP client expects.

NameRequiredDescriptionDefault
kindNo
riskNoinclude
agentNo
limitNo
noiseNoinclude
sinceNo
untilNo
domainNo
redactNo
sessionNo
project_dirNo

Output Schema

NameRequiredDescription

No output parameters

planA

Normalized plan atoms for a session — final vs drafts, grouped by task.

Wraps query(type="plan_event", …) and normalizes every agent's plan signal (Claude ExitPlanMode / Write plans/*.md, Codex update_plan, Antigravity implementation_plan.md) into a single :class:~ai_r.events.Plan shape — the per-agent signal is an internal detail, never surfaced.

Plans are grouped by task keyed on each plan's task_key — the plan-file slug when the agent has one (Claude plans/<slug>.md, Antigravity implementation_plan.md path), falling back to the normalized title only when no plan file exists (Codex update_plan). Within a task the latest plan is final and earlier revisions are draft; plans of earlier completed tasks are completed_major.

F3.4 default schema (measured ≈×3.7 cheaper than "everything inlined"): the final plan's full text is inlined (body + body_source — "approval_edited_by_user" when the user's approval carried an edited plan, which is the AUTHORITATIVE text and overrides the signal/file body, else "plan_signal"); drafts stay references (bodies via get_body); every «plan quote → user comment» pair extracted from the user's plan responses is returned under feedback, each with a ref ("<session>:pf<N>") that get_body resolves to the FULL raw response. Only agents with an interactive plan-approval flow have the feedback signal (today: Claude — an ExitPlanMode verdict or a rejected plan-file Write); others honestly contribute nothing. Technical failures and bare no-comment rejections are filtered out.

F3.4 v2 additions: every plan atom carries version — its 1-based revision number within the task group, chronological (drafts are v1…vN-1, the final is vN); every feedback pair carries plan_version (the answered revision's number), round (1-based feedback-round number within the session — one round per user response that produced pairs) and section — the heading of the plan section the quote anchors to. Quotes are selected from the RENDERED plan, so the anchor match strips markdown markup from both sides; a quote that matches no section — or more than one — gets an honest null anchor, never a nearest guess.

Args: session: Restrict to one session uuid (recommended). kind: Optional filter — draft | final | completed_major. group: Grouping strategy; only "task" is supported. agent: Optional agent filter (claude/codex/opencode/antigravity/pi). redact: When True (default) secrets in the emitted plan/feedback fields (title/steps/body/quote/comment…) are masked as [REDACTED_<TYPE>] and the response carries a redactions type→count dict when any replacement happened; False returns raw content. bodies: "final" (default) inlines the final plan's full text; "none" returns reference-only atoms. feedback: True (default) adds the feedback pair list + feedback_count; False omits both (historical shape). rounds: "all" (default) returns every feedback round; "last" keeps only each session's final round (v2). Any other value fails loud.

Returns: {"plans": [...], "count": N, "feedback": [...], "feedback_count": M} — each plan carries id/session_id/agent/title/task_id/kind/version/path/steps/status/ refs/sha256 (+ body/body_source on the final when bodies="final"); each feedback pair carries session_id/agent/plan_id/plan_version/verdict/round/quote/comment/ section/ref/ts (verdict ∈ rejected | stay_in_plan_mode; quote is null for a free-text comment; plan_version/ section are null without a signal). Draft bodies and raw responses stay on-demand via :func:get_body. Standard {"error": ..., "message": ...} dict on invalid arguments.

NameRequiredDescriptionDefault
kindNo
agentNo
groupNotask
bodiesNofinal
redactNo
roundsNoall
sessionNo
feedbackNo

Output Schema

NameRequiredDescription

No output parameters

queryA

Filter/search the unified session event stream — the workhorse verb.

Every parser's messages + tool calls are normalized into one flat, agent-neutral event stream (user_turn / assistant_turn / tool_call(<sub>) / plan_event); this tool filters that stream by facets — all behaviour is parameters, never hard-wired variants.

Facets:

  • type — user_turn | assistant_turn | tool_call | tool_call(edit|write|read|bash|other) | plan_event. Bare tool_call matches every subtype.

  • agent — one of claude/codex/opencode/antigravity/pi (all if omitted).

  • session — restrict to a single session uuid, OR a list of uuids (the union of those sessions' events in one call — e.g. the ids picked from a search_sessions / list_sessions result). Duplicates collapse; an unknown uuid contributes nothing. An empty list or a non-string item is a fail-loud invalid_argument — never a silent unfiltered scan.

  • since / until — ISO-8601 bounds (inclusive) on the event ts.

  • file — substring matched against an event's referenced file path.

  • tool — substring (pattern) matched against the referenced tool name OR the resolved name under a wrapper (tool_resolved) — so tool="commit" also finds the Skill call that ran the commit skill.

  • tool_kind — exact match against the wrapper-aware classification of a tool call: edit / write / read / bash / task (subagent spawn) / skill / mcp / web / other. Every tool_call event carries tool_kind (in refs and as a top-level field); wrappers whose input names the real actor also carry tool_resolved — the subagent type under Task/Agent/ spawn_agent, the skill name under Skill/SlashCommand, or "<server>:<tool>" for a Claude-style mcp__<server>__<tool> call. No signal → no tool_resolved (never guessed). An unknown tool_kind value is a fail-loud invalid_argument.

  • model — exact, case-insensitive match against the model that produced the event's message: an assistant_turn / tool_call / plan_event inherits the model of the assistant message behind it and carries it as a top-level model field (absent without a signal — user turns, Antigravity — so aggregate(group_by="model") buckets those under "(unknown)"). Model ids are agent-defined strings (no fixed vocabulary); events without a signal never match; an empty string is a fail-loud invalid_argument.

  • user_ref — filter user_turn events by the entity the user referenced. A string with special values: "any" matches every user turn that referenced some target; a bare kind (e.g. "file" / "session") matches turns referencing that kind of target; any other string is a substring matched against the referenced target. Non-user events never match, so combining user_ref with a non-user_turn type yields an honest empty result.

  • has_thinking — filter by whether the event's message carried model reasoning (Event.has_thinking). True keeps only events with reasoning, False only those without; unset (None, default) does not filter. Note the reasoning text itself is never inlined — this only gates on its presence (fetch it with get_body(id, include_thinking=True)).

  • text — substring matched against event text. With sort="relevance" survivors are BM25-ranked using the same scorer as search_sessions; sort="semantic" (F5.1, optional ai-r[semantic]) re-ranks the BM25 top-50 candidates by meaning with a local multilingual embedding model (cross-lingual ru↔en, synonyms) — the response carries a semantic dict reporting either the active ranking (active: true, model, candidate count, blend weight) or the honest degradation (active: false + plain-words reason + fallback: "bm25" — the order is then plain BM25, never a crash); sort="date" (default) orders by timestamp ascending.

  • relative_to (event id) + direction (prev|next) + n (a positive integer, default 1, or "all") — the neighbouring-turn walk. A numeric string ("3") is deprecated and will be rejected in 0.6.0 — pass an int or "all". Generalises the previous_user_intent used by find_file_edits to both directions and any count. step_type chooses which event type to collect (default user_turn). When relative_to is set, other filter facets are ignored.

with_intent=True attaches a top-level intent (the request behind the event, via the same previous_user_intent walk-back the legacy tools use) to every returned event. Default False keeps the base event shape unchanged.

noise filters at the session level before events are read — a session is noise when it is a spawned subagent (kind == "subagent" or parent_uuid set): "include" (default, no filtering), "exclude" (top-level sessions only), "only" (subagent sessions only). Ignored on the relative_to walk, like every other filter facet.

project_dir also filters at the session level: keep only events of sessions whose project_dir equals this path or is a descendant of it (path-boundary aware, trailing slashes ignored) — "events of this project". Sessions without a project_dir signal never match. Ignored on the relative_to walk, like every other filter facet.

parent also filters at the session level: keep only events of sessions that are a descendant (transitively, any depth) of this session uuid in the subagent parent_uuid tree — the whole spawned subtree below parent (direct children plus nested). parent itself is excluded (its own events are reachable via session=<parent>). An unknown uuid matches nothing (honest empty result). Ignored on the relative_to walk, like every other filter facet.

group filters at the event level, plan_events only: keep only the plan_events whose task_id (the plan-task grouping key — plan-file slug or normalized title) equals this value. Non-plan events never match when group is set, so combining group with a non-plan type yields an honest empty result.

redact=True (default) masks secrets in the emitted text / intent fields as [REDACTED_<TYPE>] and adds a top-level redactions type→count dict when any replacement happened; redact=False returns raw content. Redaction is emission-time only: the text facet (and every other filter) matches the RAW stored text.

Events are reference-by-default: each emitted event's text is a preview cut to ~160 chars (applied after redaction). A real cut is marked with a trailing … and text_truncated: true (absent when nothing was cut). id/refs/sha256 are untouched — fetch the full body on demand with get_body(id).

kind was removed — it duplicated noise (noise="only" for subagents, noise="exclude" for top-level). It is kept in the signature only as a fail-loud tombstone: passing any value returns an invalid_argument error pointing at noise rather than silently ignoring it (the MCP transport would otherwise drop an unknown argument and return an unfiltered result — a silent wrong answer).

Returns {"events": [...], "count": N} or the standard {"error": ..., "message": ...} dict on invalid arguments. When count == 0 the dict additionally carries diagnostics (scanned agents + session counts, corpus date bounds, cause hints) so an empty result is explainable.

NameRequiredDescriptionDefault
nNo
fileNo
kindNo
sortNodate
textNo
toolNo
typeNo
agentNo
groupNo
limitNo
modelNo
noiseNoinclude
sinceNo
untilNo
parentNo
redactNo
sessionNo
user_refNo
directionNoprev
step_typeNouser_turn
tool_kindNo
project_dirNo
relative_toNo
with_intentNo
has_thinkingNo

Output Schema

NameRequiredDescription

No output parameters

quotesA

User quotes of prior in-session content — the quotes preset.

One call answers "where did the user quote something the agent said, and what did they say about it?". When a user selects a chunk of a prior message and comments on it (the "attach selection as context" flow), the quoted text is embedded VERBATIM in their turn — no agent records it as a structured field — so it is recovered by matching the user turn against the text before it. A preset over the existing core, not a second engine: query scans supply user turns + assistant turns, the reviewed text is normalized (_normalize_rendered_text, reused from the plan-feedback anchorer) and :func:difflib.SequenceMatcher finds the longest verbatim run; a run below the minimum is not a quote (honest null), and text pasted from OUTSIDE the session matches nothing (never fabricated).

This is the cross-agent, chat-wide generalization of plan(feedback) (which surfaces «plan quote → user comment» only for Claude's plan-approval flow): quotes surfaces «any prior message quote → user comment» for every agent, operating on the normalized event stream, not client markup.

Filters (all parameters): agent, session (uuid or list), since/until (ISO bounds on the user turn's ts), source_kind (currently assistant — a user quoting the agent's prose; unknown values fail loud), noise and project_dir (session-level, same as query).

Each record carries the user_turn event id (context on-demand via query(relative_to=...)), source_id (the quoted assistant turn), source_kind, quote_chars, and the char-capped quote + comment (the user's turn with the quote elided). count/by_source_kind reflect the FULL matched set; limit (default 50, 0 = no cap) bounds only the emitted records (truncated).

Caveat (documented): v1 sources are assistant prose (the common case — quoting a tool's raw output is a future extension); the emitted quote/comment come from the NORMALIZED text (markdown stripped), so they are readable but not byte-identical to the raw turn (raw bodies stay reachable via the event ids). redact=true (default) masks secrets only in the emitted session_title/quote/comment. When count == 0 the response carries diagnostics so an empty result is explainable.

Thin wrapper over :func:ai_r.quotes.quotes that translates the core ValueError contract into the {"error": "invalid_argument", "message": str(exc)} shape the MCP client expects.

NameRequiredDescriptionDefault
agentNo
limitNo
noiseNoinclude
sinceNo
untilNo
redactNo
sessionNo
project_dirNo
source_kindNo

Output Schema

NameRequiredDescription

No output parameters

read_sessionA

Read a single session by uuid; agent is an optional hint.

Args: uuid: Session identifier. agent: One of claude, codex, opencode, antigravity, pi. Optional: when omitted, the uuid is looked up across every parser (session ids are unique across agents in practice). If — rarely — the same id exists under several agents, a candidates list is returned (not an error) so the caller can re-ask with an explicit agent. offset: Zero-based index of the first message to return (applied to the projected {role, content} list). limit: Maximum number of messages to return. Defaults to :data:_MESSAGES_CAP (100). A non-positive value means "no upper bound". redact: When True (default) secrets in the emitted title, message content/intent/qa are masked as [REDACTED_<TYPE>] and the response carries a redactions type→count dict when any replacement happened (see ai_r.redact); False returns the raw content. with_tokens: When True (F3.3) attach token usage read at request time (nothing runs in the background):

    * ``summary["tokens"]`` — the session's flat
      :func:`ai_r.tokens.session_tokens` block (exact where the agent
      records usage, a labeled estimate otherwise, honest
      ``source=None`` without any signal);
    * ``summary["component_tokens"]`` — the
      :func:`ai_r.tokens.component_tokens` breakdown: the transcript's
      estimated token volume split across ai-r's event taxonomy
      (``user_turn`` / ``assistant_turn`` / ``thinking`` / ``plan``
      and a ``tool_call`` per-``tool_kind`` sub-dict), always
      ``source="estimate"`` (never merged with the exact tier),
      ``None`` on an empty transcript;
    * per-message ``tokens`` on projected entries that carry exact
      usage — Claude (deduplicated per streamed API call), OpenCode
      and Pi.  Codex / Antigravity / user turns carry **no**
      ``tokens`` key at all (absent, not ``null``).  The dedup/attach
      is decided on absolute message positions BEFORE pagination, so
      page boundaries never shift which record is "first" for a call.

    Default ``False``: output is byte-identical to the historical
    shape (no ``tokens`` / ``component_tokens`` key on the summary or
    any message).  The token blocks carry only integers and
    ai-r-authored labels (never raw session text), so they stay
    outside the F2.1 redaction pass by construction.
include_subagents: When ``True`` attach
    ``summary["subagent_rollup"]`` — the parent session's
    ``component_tokens`` block plus one per spawned subagent child
    (resolved via :func:`ai_r.session_stats.children_of` on
    ``parent_uuid``) and a ``total`` folding parent + children through
    the ``aggregate`` ``component_tokens`` metric.  Each child entry
    carries ``uuid`` / ``agent`` / ``component_tokens`` (the estimate)
    plus **what it actually cost**: ``tokens`` read from the child's own
    transcript on the honest three-tier ``source`` ladder — billed
    ``"exact"`` where the child's transcript records usage, a labeled
    ``"estimate"`` where it does not (a truncated / reference-only run),
    ``source=None`` without any signal (never a fabricated zero) —
    ``models`` (the model(s) it ran on — a persona pinned to a cheaper
    tier shows up here), ``subagent_type`` (its persona, from the
    child's own spawn metadata, falling back to the spawning call's
    sidecar) and ``status`` where the spawn recorded one
    (``"async_launched"`` for a background spawn).  A childless parent
    (or an agent like Antigravity that never records ``parent_uuid``)
    yields an empty ``children`` list and a ``total`` equal to the
    parent's own block — honest, not an error.  Independent of
    ``with_tokens``.  Default ``False``.
include_thinking: When ``True`` attach the model's reasoning
    (``message.thinking``) as a separate ``thinking`` string field on
    each projected message that carried it — kept OUT of ``content``
    so the historical text projection is unchanged.  Default
    ``False``: output is byte-identical to the historical shape (no
    ``thinking`` key on any message).  Reasoning is opt-in to save the
    caller's budget; ``Event.has_thinking`` / the presence of the key
    signals which messages have it.  Fail-soft: a message without
    reasoning never carries the key.

Returns: A dict with session metadata plus:

* ``messages`` — the projected ``{role, content}`` list, sliced
  to ``[offset:offset+limit]``.
* ``total`` — the full uncapped projected message count (the
  length the slice was taken from).
* ``offset`` / ``limit`` — the pagination echo values actually
  used.
* ``messages_truncated`` — True when the MCP hard cap stopped
  extraction before every projected message could be returned.
* ``outcome`` — session outcome classification (F2.3):
  ``{status: success|failure|mixed|unknown, signals, user_verdict,
  markers, tool_results, tool_errors, error_rate,
  error_rate_reliable}``.  ``status`` combines the tool-call
  error rate (real flag only for Claude/OpenCode —
  ``None`` elsewhere, never guessed) with a calibrated bilingual
  success/failure dictionary over the tail user turns;
  ``"unknown"`` when neither signal exists (SSOT
  :mod:`ai_r.outcome`).

On an id collision (agent omitted, several agents own the id):
``{"ambiguous": True, "uuid": ..., "candidates": [...],
"count": N, "message": ...}`` where each candidate is a session
summary carrying its ``agent``.

On a missing session, returns an ``error`` dict instead of
raising (``agents_scanned`` lists the parsers probed when the
agent was omitted).
NameRequiredDescriptionDefault
uuidYes
agentNo
limitNo
offsetNo
redactNo
with_tokensNo
include_thinkingNo
include_subagentsNo

Output Schema

NameRequiredDescription

No output parameters

search_sessionsA

Case-insensitive search across sessions.

Args: query: Search string. Supports: * Bare words: pwa manifest (AND default) * Quoted phrases: "exact phrase" * Negative prefix: -claude (Google-style, always excluded) agent: Optional agent filter (claude/codex/opencode/antigravity/pi). scope: Where to look. * "title" — only session.title (default, backward-compat) * "body" — message text + tool_use[*].input + tool_result[*].content * "all" — title OR body operator: How to combine positive terms. * "AND" — all positive terms must appear (default) * "OR" — at least one positive term must appear * "NOT" — no term (positive or negative) may appear Negative -term prefixes are always excluded regardless of operator. limit: Maximum number of results. 0 or negative = no limit. Applied after sorting, so it keeps the top-ranked matches. sort: Result ordering. * "relevance" — BM25 relevance over the matched text (default). Pure-stdlib scoring; ties keep newest-first. * "date" — newest-first by session date (the historical pre-ranking order). * "semantic" — F5.1 (optional ai-r[semantic]): the BM25 top-50 candidates re-ranked by meaning with a local multilingual embedding model (cross-lingual ru↔en, synonyms); the response carries a semantic dict — either the active ranking (active: true, model, candidate count, blend weight) or the honest degradation notice (active: false + plain-words reason + fallback: "bm25", order stays BM25 — never a crash). noise: Noise filter — a session is noise when it is a spawned subagent (kind == "subagent" or parent_uuid set). * "include" — no filtering (default). * "exclude" — search only top-level agent sessions. * "only" — search only subagent sessions. Applied before matching, so excluded sessions never pay the body-scan cost. redact: When True (default) secrets in the emitted title / snippet / extra fields are masked as [REDACTED_<TYPE>] and the response carries a redactions type→count dict when any replacement happened; False returns raw content. Matching always runs on the RAW stored text, so searching for a literal secret still finds its session — only the displayed snippet is masked. include_thinking: When True fold model reasoning (message.thinking) into the body/all search haystack so a search matches text that lives only in the model's thoughts. Default False: reasoning is excluded from matching to save the caller's budget (turn it on only when a query must reach into reasoning). No effect on scope="title". The two modes are cached under separate keys, so toggling never serves a stale haystack of the other mode.

Returns: A dict {"results": [...], "count": N} where results is the list of session summaries and count is their total. When scope is "body" or "all" and a match is found, each summary includes a "snippet" field with the first matching message excerpt (up to 200 chars) and may carry body_truncated. When a scan matches nothing (count == 0), the dict additionally carries diagnostics (scanned agents + session counts, corpus date bounds, cause hints) so an empty result is explainable. With sort="semantic" the dict also carries a semantic report (active ranking vs BM25 fallback + reason).

Errors are returned as a top-level {"error": ..., "message": ...} dict (matches the existing convention).

NameRequiredDescriptionDefault
sortNorelevance
agentNo
limitNo
noiseNoinclude
queryYes
scopeNotitle
redactNo
operatorNoAND
include_thinkingNo

Output Schema

NameRequiredDescription

No output parameters

session_diffA

Reconstruct what the agent changed in one session — without git.

Stitches the session's own edit records (Edit/MultiEdit old_string→new_string, Write content, codex shell-exec redirections) into a per-file, chronological diff. Returns {"files": [...], "count": N, "caveats": [...]}; each file carries its ordered edits (timestamp + intent + hunks) and a stitched, readable diff.

caveats always carries two honest blind spots: (1) this is a diff of the agent's actions, not the git outcome — manual edits / partial commits / merges are invisible; (2) RISK-3 — inherits the find_file_edits shell-redirect gap (tee / sed -i / cp / mv / heredoc writes are not detected and are silently skipped).

redact=True (default) masks secrets in the emitted diff/hunks/ intents as [REDACTED_<TYPE>] and adds a redactions type→count dict when any replacement happened; redact=False returns raw.

Size-bounded output (mirrors find_file_edits): over-long intent / hunk bodies / per-file diff text are cut with a …[truncated] marker and named in the per-file truncated_fields (indexed paths), and whole-file emission stops at a total byte budget (output_truncated; count keeps the true total). The full edit body stays reachable on demand via get_body / read_session.

Thin wrapper over :func:ai_r.session_diff.session_diff that translates the core ValueError contract into the {"error": "invalid_argument", "message": str(exc)} shape the MCP client expects.

NameRequiredDescriptionDefault
pathNo
agentYes
redactNo
session_uuidYes

Output Schema

NameRequiredDescription

No output parameters

session_statsA

Summarise sessions, grouped and ranked — the bird's-eye audit view.

Where find_file_edits / find_tool_calls return flat record streams, this rolls the sessions themselves up by one dimension so you can see how the work is distributed in a single call.

group_by is one of:

  • "agent" (default) — claude vs codex vs opencode vs ...

  • "dir" — by working directory / project (the normalized project_dir first — one real directory = one bucket across agents — then cwd for codex/pi / project slug for claude; "(unknown)" for agents without any signal).

  • "date" — by calendar day (YYYY-MM-DD).

  • "kind" — top-level agent sessions vs spawned subagent sessions.

  • "model" — by the model that produced the session. A session that mixed models buckets as "(mixed)" rather than being attributed to one of them; one whose transcript records no model is "(unknown)". Neither is guessed. Pair with with_tokens=True to see what each model actually cost.

Each group carries its session count plus enrichment from the shared find_file_edits core: edits (file edits attributed to the group's sessions), intents (distinct requests behind those edits), the distinct agents in the group, and total messages.

RISK-4 note: subagent detection is currently Claude-only. When no subagent sessions are in scope, a group_by="kind" result shows a single agent bucket — so the result always carries kind_split_available (False here) plus a note making clear that this is NOT a verified "no subagents", just an absent split.

with_tokens=True (F3.3) additionally reads every matched session's token usage at request time (nothing runs in the background) and adds a folded tokens block to each group and to totals: {input, output, reasoning, cache_read, cache_write, total, exact, estimated, unknown}. Per session the numbers are exact where the agent's own files record usage (Claude message.usage, Codex token_count, OpenCode message.data.tokens, Pi usage); a session without a recorded signal (e.g. Antigravity) gets a transcript-volume estimate — tokenized with the optional tiktoken dependency (pip install "ai-r[tokens]") when installed, else a rough chars/4 heuristic — and counts under estimated, never silently mixed in as exact; no signal at all counts under unknown. Sums that no session carried stay null (never a fabricated 0). The block contains only ai-r-computed integers and labels — no raw session text — so it is outside the redaction surface by construction. Default False: byte-identical historical output, no extra reads.

Scan guard (token_scan_limit): because with_tokens reads every matched session's files at request time, an unscoped run over a huge corpus is a multi-hour I/O storm. When with_tokens is set with no narrowing filter (agent/since/until) and more than token_scan_limit sessions match, the call returns {"error": "scope_required", ...} (naming the count and the limit) INSTEAD of scanning — the check runs on the cheap inventory count before any file is read. Narrow the scope, or raise token_scan_limit (0 disables the cap) to force the full scan. A permitted-but-large scan runs but carries a warning.

Thin wrapper over :func:ai_r.session_stats.session_stats that translates the core ValueError contract into the {"error": "invalid_argument", "message": str(exc)} shape the MCP client expects.

NameRequiredDescriptionDefault
topNo
agentNo
sinceNo
untilNo
group_byNoagent
edit_pathNo/
with_tokensNo
token_scan_limitNo

Output Schema

NameRequiredDescription

No output parameters

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 18 tool updatesv0.4.2
    • First observedaggregate
    • First observedaudit_brief
    • First observeddetect_current
    • First observeddiff
    • First observedfind_file_edits
    • First observedfind_tool_calls
    • First observedget_body
    • First observedincidents
    • First observedlist_sessions
    • First observedlocate
    • First observednetwork
    • First observedplan
    • First observedquery
    • First observedquotes
    • First observedread_session
    • First observedsearch_sessions
    • First observedsession_diff
    • First observedsession_stats

TDQS

A4.1/5.0

Scored across 18 tools

Disambiguation2/5

Multiple tools serve overlapping purposes: list_sessions, locate, and search_sessions all find sessions; query, find_file_edits, and find_tool_calls all filter events; session_stats and aggregate largely duplicate each other; session_diff and diff are nearly identical. The descriptions are detailed but the boundaries are vague, making misselection likely.

Naming Consistency3/5

The set mixes verb_noun patterns (list_sessions, read_session, find_file_edits) with bare nouns (incidents, network, quotes) and noun_noun compounds (session_stats, session_diff). While each name is readable, the inconsistent conventions and generic verbs like query and aggregate reduce predictability.

Tool Count3/5

At 18 tools, the set is heavy, but many tools are explicitly presets over a core (audit_brief, incidents, network, quotes, plan, session_diff) so the count reflects a deliberate tradeoff between convenience and atomicity. The redundancy between aggregate/session_stats and query/find_* makes it feel over-scoped, but it is within the 16-25 heavy range.

Completeness4/5

The tool set covers the full audit lifecycle: discovery (list_sessions, locate, search_sessions, detect_current), reading (read_session, get_body), event search (query, find_*), aggregation (aggregate, session_stats), and specialized presets (audit_brief, incidents, network, quotes, plan). Minor gaps exist—e.g., no bulk export or cross-session diff—but the core domain is well covered.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    Serves local coding-agent session history over stdio, letting MCP-capable agents search past sessions across Codex, Claude Code, Gemini CLI, OpenCode, jcode, and Pi and read bounded, secret-redacted transcripts. Read-only mode restricts it to search and transcript tools, while write-enabled mode can create copies of a conversation in another agent's native format.
    3
    62
    MIT
  • A
    license
    B
    quality
    C
    maintenance
    Enables inspection of the local session stores kept by Codex, Claude Code, Antigravity, OpenCode, Freebuff and Grok through a single read-only interface, exposing recent sessions, individual session events, and full-history search. It also generates a one-call markdown handoff brief covering the goal, files touched, last observed state, blocker and suggested next steps, so a different agent or operator can resume work without cloud services or manual context copying.
    4
    MIT