ai-r
ai-r is a read-only MCP server that audits, searches, and extracts structured entities (plans, intents, edits, costs) from coding-agent session histories across Claude, Codex, OpenCode, Antigravity, and Pi.
List and read sessions across five agents, with filters for agent, project directory, recency/activity, and subagent kind.
Search sessions by title or body using BM25 relevance, date ordering, or optional local semantic re-ranking.
Query a unified event stream (user/assistant turns, tool calls, plan events) with filters by type, agent, file, model, tool kind, and more, plus intent attribution.
Find file edits cross-agent with authorship, timestamps, and the request/intention behind each edit.
Find tool calls by name/pattern, error status, input/output substrings, and enrichment for spawned subagents.
Detect dangerous incidents — risky bash commands (rm -rf, curl|sh, force push) and whether a confirmed rollback/apology followed.
Audit network egress — web requests with risk flags (plain HTTP, secrets in URLs, private hosts, punycode).
Extract plans — final vs. drafts per task, with user feedback, versions, and references to full bodies.
Reconstruct session diffs without git, per file, chronologically.
Compute costs and stats — tokens per session/model/group, exact where recorded, honestly estimated otherwise.
Generate audit briefs — token-lean, budgeted digests of a session for fresh-agent review.
Locate sessions by UUID, id-prefix, or title substring, with resume/read commands.
Detect the current runtime session and agent, returning a ready-to-run resume command.
Aggregate and diff programmatically — generic row folding and diff stitching for building custom audit tools.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ai-rShow me the plan from the last session — final only, no intermediate revisions."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
ai-r
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 —gitwon'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-012026-06-03 codex auth.py "add a refresh token" edit
2026-06-07 claude auth.py "extract the check into middleware" editRelated 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 (
> fileunder 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-rwon't infer a silent rollback, it honestly says "not confirmed".lied about the plan —
ai-rdoesn'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.
/compacterases 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-rgives 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 |
|
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 — |
Audit replay (reconstruct a session's changes, no git) | — | Rarely | Yes — |
Plan extraction (final vs draft, normalized) | — | — | Yes — |
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 |
| JSONL |
Codex |
| JSONL |
OpenCode |
| SQLite (snap/flatpak auto-detect) |
Antigravity |
| JSON / markdown brain directories |
Pi |
| 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 thehttpextra 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, typedSession/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.shThe 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-mcpover 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). WithAI_R_MCP_TRANSPORT=httpa single warm server on localhost (default127.0.0.1:8756) is shared by every agent instead of a swarm; the systemd units inpackaging/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=1and anAI_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 anAuthorization: Bearer <token>header.Knobs (env):
AI_R_MCP_PORT— port (default8756).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 (default1): the long-lived shared server outlives its clients, so a request with a staleMcp-Session-Idis served instead of being rejected with404 "Session not found";0restores 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 byquery/get_body/audit_brief/ the plan projections): ceiling by entry count (default8) 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
httpextra 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
Method vocabulary (verbs + presets) —
docs/methods.md(English SSOT) ·docs/methods.ru.md(Russian mirror)Acceptance scenarios (104 e2e) —
docs/scenarios.mdArchitecture & layering —
docs/architecture.mdSearch operators —
docs/search-operators.mdPer-agent MCP registration —
docs/mcp-registration.mdParser coverage & limitations —
docs/parsers.mdSecurity (untrusted content) —
docs/security.mdAdd a sixth agent —
CONTRIBUTING.md
Development
git clone https://github.com/pro-target/ai-r.git
cd ai-r
pip install -e ".[dev]"
pytest --cov=src/ai_r1300+ 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.mdConventional 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 toolsaggregateA
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.
| Name | Required | Description | Default |
|---|---|---|---|
| rows | Yes | ||
| metrics | No | ||
| rank_by | No | default | |
| group_by | Yes | ||
| kind_split | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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"}.
| Name | Required | Description | Default |
|---|---|---|---|
| agent | No | ||
| redact | No | ||
| session | Yes | ||
| budget_chars | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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.
| Name | Required | Description | Default |
|---|---|---|---|
| agent | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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.
| Name | Required | Description | Default |
|---|---|---|---|
| rows | Yes | ||
| format | No | unified | |
| redact | No | ||
| per_file | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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). Passinclude_input=Trueto inline the full body underinputinstead.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| agent | No | ||
| limit | No | ||
| since | No | ||
| until | No | ||
| redact | No | ||
| include_input | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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.
| Name | Required | Description | Default |
|---|---|---|---|
| agent | No | ||
| limit | No | ||
| since | No | ||
| until | No | ||
| redact | No | ||
| session | No | ||
| is_error | No | ||
| tool_name | No | ||
| output_mode | No | ||
| input_contains | No | ||
| output_contains | No | ||
| output_excludes | No | ||
| tool_name_pattern | No | ||
| with_subagent_cost | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | ||
| redact | No | ||
| shallow | No | ||
| max_chars | No | ||
| include_thinking | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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.
| Name | Required | Description | Default |
|---|---|---|---|
| agent | No | ||
| limit | No | ||
| noise | No | include | |
| since | No | ||
| until | No | ||
| redact | No | ||
| session | No | ||
| category | No | ||
| confirmed | No | include | |
| project_dir | No | ||
| reaction_window | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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: transcriptcwd/ Desktop metadata / verified slug decode; Codex:session_meta.cwd; OpenCode:session.directory; Pi: headercwd; Antigravity: no signal).launch_surface— where the session was driven from (Claude:"claude-cli"|"claude-desktop"; Codex: the raworiginator, 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 asdate;dateis kept for backward compatibility);age_sec— whole seconds sincelast_activity(clamped at0when a future timestamp implies writer/reader clock skew);activity—"fresh"ifage_secis at or under theAI_R_STALL_SECthreshold (default600s = 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.
| Name | Required | Description | Default |
|---|---|---|---|
| kind | No | ||
| agent | No | ||
| limit | No | ||
| noise | No | include | |
| offset | No | ||
| redact | No | ||
| project_dir | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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"}).
| Name | Required | Description | Default |
|---|---|---|---|
| web | No | ||
| agent | No | ||
| limit | No | ||
| needle | Yes | ||
| redact | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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.
| Name | Required | Description | Default |
|---|---|---|---|
| kind | No | ||
| risk | No | include | |
| agent | No | ||
| limit | No | ||
| noise | No | include | |
| since | No | ||
| until | No | ||
| domain | No | ||
| redact | No | ||
| session | No | ||
| project_dir | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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.
| Name | Required | Description | Default |
|---|---|---|---|
| kind | No | ||
| agent | No | ||
| group | No | task | |
| bodies | No | final | |
| redact | No | ||
| rounds | No | all | |
| session | No | ||
| feedback | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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. Baretool_callmatches 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 asearch_sessions/list_sessionsresult). Duplicates collapse; an unknown uuid contributes nothing. An empty list or a non-string item is a fail-loudinvalid_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) — sotool="commit"also finds the Skill call that ran thecommitskill.tool_kind— exact match against the wrapper-aware classification of a tool call:edit/write/read/bash/task(subagent spawn) /skill/mcp/web/other. Everytool_callevent carriestool_kind(inrefsand as a top-level field); wrappers whose input names the real actor also carrytool_resolved— the subagent type under Task/Agent/ spawn_agent, the skill name under Skill/SlashCommand, or"<server>:<tool>"for a Claude-stylemcp__<server>__<tool>call. No signal → notool_resolved(never guessed). An unknowntool_kindvalue is a fail-loudinvalid_argument.model— exact, case-insensitive match against the model that produced the event's message: anassistant_turn/tool_call/plan_eventinherits the model of the assistant message behind it and carries it as a top-levelmodelfield (absent without a signal — user turns, Antigravity — soaggregate(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-loudinvalid_argument.user_ref— filteruser_turnevents 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 combininguser_refwith a non-user_turntypeyields an honest empty result.has_thinking— filter by whether the event's message carried model reasoning (Event.has_thinking).Truekeeps only events with reasoning,Falseonly those without; unset (None, default) does not filter. Note the reasoning text itself is never inlined — this only gates on its presence (fetch it withget_body(id, include_thinking=True)).text— substring matched against event text. Withsort="relevance"survivors are BM25-ranked using the same scorer assearch_sessions;sort="semantic"(F5.1, optionalai-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 asemanticdict reporting either the active ranking (active: true, model, candidate count, blend weight) or the honest degradation (active: false+ plain-wordsreason+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, default1, 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 theprevious_user_intentused byfind_file_editsto both directions and any count.step_typechooses which event type to collect (defaultuser_turn). Whenrelative_tois 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.
| Name | Required | Description | Default |
|---|---|---|---|
| n | No | ||
| file | No | ||
| kind | No | ||
| sort | No | date | |
| text | No | ||
| tool | No | ||
| type | No | ||
| agent | No | ||
| group | No | ||
| limit | No | ||
| model | No | ||
| noise | No | include | |
| since | No | ||
| until | No | ||
| parent | No | ||
| redact | No | ||
| session | No | ||
| user_ref | No | ||
| direction | No | prev | |
| step_type | No | user_turn | |
| tool_kind | No | ||
| project_dir | No | ||
| relative_to | No | ||
| with_intent | No | ||
| has_thinking | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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.
| Name | Required | Description | Default |
|---|---|---|---|
| agent | No | ||
| limit | No | ||
| noise | No | include | |
| since | No | ||
| until | No | ||
| redact | No | ||
| session | No | ||
| project_dir | No | ||
| source_kind | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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).
| Name | Required | Description | Default |
|---|---|---|---|
| uuid | Yes | ||
| agent | No | ||
| limit | No | ||
| offset | No | ||
| redact | No | ||
| with_tokens | No | ||
| include_thinking | No | ||
| include_subagents | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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).
| Name | Required | Description | Default |
|---|---|---|---|
| sort | No | relevance | |
| agent | No | ||
| limit | No | ||
| noise | No | include | |
| query | Yes | ||
| scope | No | title | |
| redact | No | ||
| operator | No | AND | |
| include_thinking | No |
Output Schema
| Name | Required | Description |
|---|---|---|
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.
| Name | Required | Description | Default |
|---|---|---|---|
| path | No | ||
| agent | Yes | ||
| redact | No | ||
| session_uuid | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
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 normalizedproject_dirfirst — one real directory = one bucket across agents — thencwdfor 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 withwith_tokens=Trueto 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.
| Name | Required | Description | Default |
|---|---|---|---|
| top | No | ||
| agent | No | ||
| since | No | ||
| until | No | ||
| group_by | No | agent | |
| edit_path | No | / | |
| with_tokens | No | ||
| token_scan_limit | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
18 tool updates
v0.4.2- First observed
aggregate - First observed
audit_brief - First observed
detect_current - First observed
diff - First observed
find_file_edits - First observed
find_tool_calls - First observed
get_body - First observed
incidents - First observed
list_sessions - First observed
locate - First observed
network - First observed
plan - First observed
query - First observed
quotes - First observed
read_session - First observed
search_sessions - First observed
session_diff - First observed
session_stats
TDQS
Scored across 18 tools
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.
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.
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.
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
Related MCP Connectors
Cross-agent artifact workspace with provenance across Claude Code, Codex, Cursor, LangGraph.
Query your team's AI coding history and act on HypAware recommendations from any MCP client.
One searchable history across every AI coding tool, with secret scanning and a shared task board.
Team memory for AI coding agents and code reviews, reviewed by humans and served over MCP.
Related MCP Servers
- AlicenseAqualityDmaintenanceLocal-first, read-only Codex session aggregator that indexes multiple CODEX_HOME directories into a SQLite database and provides MCP tools for cross-project, archival, and sub-agent history queries.53MIT
- AlicenseAqualityCmaintenanceProvides MCP tools to search, browse, and retrieve unified read-only history from CLI agents like Codex, Claude Code, and OpenCode, with connector discovery and project-folder grouping.61MIT
- AlicenseAqualityBmaintenanceServes 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.362MIT
- AlicenseBqualityCmaintenanceEnables 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.4MIT