Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,38 @@ A symbol fixed often and recently scores near 1.0. A quiet area decays by half e

The longer Graphora runs on a repository, the smarter it gets. That compounds.

## Agent session memory: graph your work, not just your code

AI coding agents already record every session locally: GitHub Copilot CLI
(`~/.copilot/session-store.db`), Claude Code (`~/.claude/projects/`), Codex CLI
(`~/.codex/sessions/`). Graphora ingests them all into **one graph**, so sessions from
different terminals · and different agents · become connected the moment they touch the
same file, repo, or PR. Cross-agent memory: "which agent touched build.yml?"

```bash
graphora sessions ingest --days 7 # all agents found (or --source copilot|claude|codex)

graphora sessions connected file build.yml # which windows/agents touched this file?
graphora sessions connected ref 275 # which sessions relate to PR 275?
graphora sessions connected repo org/proj # everything that happened in one repo
```

No FalkorDB container? Same commands work with the embedded JSON backend:

```bash
graphora sessions ingest --days 7 --backend embedded
graphora sessions connected file build.yml --backend embedded
```

Make it zero-effort: install the session-recall skill so your agents run these
commands themselves when you ask "which sessions touched this file?":

```bash
graphora install-skill all --skill sessions # or --skill all for code + sessions
```

Read-only on the sources, no LLM, idempotent. See [use case 6](USECASES.md).

## Benchmarks

Measured on real repositories, fully deterministic, zero network. Full methodology and reproduction commands in [BENCHMARKS.md](./BENCHMARKS.md).
Expand Down
76 changes: 75 additions & 1 deletion USECASES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Graphora Use Cases

Five real scenarios, all executed locally and captured verbatim on 2026-07-14.
Six real scenarios, all executed locally and captured verbatim (use cases 1–5 on
2026-07-14, use case 6 on 2026-07-16).
Environment: macOS, Python 3.11, FalkorDB in Docker on `localhost:6379`.

Setup used for all of them:
Expand Down Expand Up @@ -168,6 +169,79 @@ trustworthy enough to gate merges on.

---

## Use case 6: agent session memory as a graph

**Scenario**: you work with AI coding agents in many terminal tabs at once. Each tab has
its own context, and after a day you no longer remember which window did what, which
sessions touched the same file, or which ones relate to the PR you're reviewing.

The Copilot CLI already records every session in a local SQLite store
(`~/.copilot/session-store.db`): summaries, messages, files touched, PR/issue/commit
references. Graphora ingests it as just another data source — no code parsing, same
graph, same tooling:

```bash
graphora sessions ingest --days 7
```

Captured output against a real workstation store (2026-07-16):

```json
{
"project": "agent-sessions",
"graph": "graphora:agent-sessions",
"sessions": 19,
"files_touched": 158,
"refs": 24
}
```

The graph is `(:Session)-[:TOUCHED]->(:WorkFile)`, `(:Session)-[:IN_REPO]->(:Repo)`,
`(:Session)-[:REFERENCES]->(:Ref)`. Sessions from different terminals become connected
the moment they touch the same file, repo, or PR. Then ask relationship questions that
are painful in SQL and trivial in Cypher:

```bash
graphora sessions connected file build.yml # which windows touched this file?
graphora sessions connected ref 275 # which sessions relate to PR 275?
graphora sessions connected repo org/proj # everything that happened in one repo
```

**How to use it** (step by step):

1. `pip install graphora-kg`
2. Have FalkorDB running (`docker run -d -p 6379:6379 falkordb/falkordb`), **or** skip
Docker entirely and add `--backend embedded` to every command below.
3. `graphora sessions ingest --days 7` — ingests every agent store found on the machine
(Copilot CLI, Claude Code, Codex CLI), skips missing ones, safe to re-run anytime.
Use `--source copilot|claude|codex` to ingest just one.
4. Ask away: `graphora sessions connected file <name>` / `ref <pr-number>` / `repo <name>`.
Each hit is tagged with the agent that did the work — cross-agent memory.
5. Zero-effort mode: `graphora install-skill all --skill sessions` teaches your agents
(22 supported) to run these commands themselves when you ask about past work.
6. Optional: `graphora serve-mcp --project agent-sessions` exposes your work history to
any MCP-capable agent.

Captured output:

```json
[
{
"session": "007d10dd",
"summary": "Add Docker Build Completion Check",
"updated_at": "2026-07-14T07:47:52.822Z",
"via": "/Users/naseemali/Documents/GitHub/FalkorDB/.github/workflows/build.yml"
}
]
```

**Why it matters**: tools like graphify map your *code*; Graphora also maps your *work*.
Ingestion is deterministic and read-only on the source (no LLM, no writes to the session
store), idempotent (re-ingesting never duplicates nodes), and reuses the live FalkorDB
graph, so the MCP server exposes your work history to any agent for free.

---

## How it was tested <a name="how-it-was-tested"></a>

Test-first at every phase; the full suite had to pass before the next phase started.
Expand Down
50 changes: 49 additions & 1 deletion graphora/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,44 @@ def cmd_install_skill(args: argparse.Namespace) -> int:
print("\n".join(list_agents()))
return 0
agents = None if not args.agents or args.agents == ["all"] else args.agents
kinds = ["code", "sessions"] if args.skill == "all" else [args.skill]
try:
written = install_skill(args.repo, agents)
written = []
for kind in kinds:
written.extend(install_skill(args.repo, agents, skill=kind))
except ValueError as exc:
print(str(exc), file=sys.stderr)
return 1
print(json.dumps({"repo": str(Path(args.repo).resolve()), "written": written}, indent=2))
return 0


def cmd_sessions_ingest(args: argparse.Namespace) -> int:
from graphora.sessions import ingest_sources

store = open_store(args.project or "agent-sessions", backend=args.backend, host=args.host, port=args.port)
sources = None if args.source == "all" else [args.source]
paths = {}
if args.db:
paths["copilot"] = args.db
if args.claude_root:
paths["claude"] = args.claude_root
if args.codex_root:
paths["codex"] = args.codex_root
results = ingest_sources(store, sources=sources, days=args.days, paths=paths)
print(json.dumps({"project": store.project, "graph": store.graph_name, "sources": results}, indent=2))
return 0


def cmd_sessions_connected(args: argparse.Namespace) -> int:
from graphora.sessions import connected

store = open_store(args.project or "agent-sessions", backend=args.backend, host=args.host, port=args.port)
rows = connected(store, args.kind, args.value)
print(json.dumps(rows, indent=2))
return 0


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="graphora", description="Deterministic code knowledge graph tool")
parser.add_argument("--version", action="version", version=f"graphora {__version__}")
Expand Down Expand Up @@ -202,10 +231,27 @@ def build_parser() -> argparse.ArgumentParser:
p_mcp = sub.add_parser("serve-mcp", parents=[common], help="Serve the graph as an MCP stdio server")
p_mcp.set_defaults(func=cmd_serve_mcp)

p_sess = sub.add_parser("sessions", parents=[common], help="Agent session-history graph commands")
sess_sub = p_sess.add_subparsers(dest="sessions_command", required=True)
p_si = sess_sub.add_parser("ingest", parents=[common], help="Ingest agent session stores into the graph")
p_si.add_argument("--source", choices=["all", "copilot", "claude", "codex"], default="all",
help="Which agent's sessions to ingest (default: all found)")
p_si.add_argument("--db", default=None, help="Copilot session-store.db path (default: ~/.copilot/session-store.db)")
p_si.add_argument("--claude-root", default=None, help="Claude Code projects dir (default: ~/.claude/projects)")
p_si.add_argument("--codex-root", default=None, help="Codex CLI sessions dir (default: ~/.codex/sessions)")
p_si.add_argument("--days", type=int, default=30, help="Only sessions updated in the last N days")
p_si.set_defaults(func=cmd_sessions_ingest)
p_sc = sess_sub.add_parser("connected", parents=[common], help="Sessions connected to a file, repo, or PR/issue/commit")
p_sc.add_argument("kind", choices=["file", "repo", "ref"])
p_sc.add_argument("value")
p_sc.set_defaults(func=cmd_sessions_connected)

p_skill = sub.add_parser("install-skill", help="Install the Graphora skill/rule for AI coding agents")
p_skill.add_argument("agents", nargs="*", help="Agent names, or 'all' (default: all)")
p_skill.add_argument("--repo", default=".", help="Repository root to install into (default: cwd)")
p_skill.add_argument("--list", action="store_true", help="List supported agents")
p_skill.add_argument("--skill", choices=["code", "sessions", "all"], default="code",
help="Which skill to install: code graph workflow, session recall, or both")
p_skill.set_defaults(func=cmd_install_skill)

return parser
Expand All @@ -218,6 +264,8 @@ def main(argv: list[str] | None = None) -> int:
args.project = Path(args.repo).resolve().name
elif args.command == "risk" and getattr(args, "path", None):
args.project = Path(args.path).resolve().name
elif args.command == "sessions":
args.project = "agent-sessions"
else:
args.project = Path.cwd().name
return args.func(args)
Expand Down
49 changes: 49 additions & 0 deletions graphora/embedded.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ def _empty() -> dict:
"fix_commits": {}, # sha -> {date, subject, kind}
"touched": [], # [sha, path]
"fixed": [], # [sha, stable_key]
"sessions": {}, # sid -> {summary, cwd, branch, created_at, updated_at, last_ask}
"session_repos": [], # [sid, repo]
"session_files": [], # {sid, path, tool}
"session_refs": [], # [sid, kind, value]
}

@property
Expand Down Expand Up @@ -184,6 +188,51 @@ def stats(self) -> dict[str, int]:
def has_files(self) -> bool:
return bool(self._data["files"])

# --- agent sessions (see graphora/sessions.py) -------------------------

def upsert_session(self, sid: str, props: dict) -> None:
self._data.setdefault("sessions", {})[sid] = dict(props)

def link_session_repo(self, sid: str, repo: str) -> None:
edges = self._data.setdefault("session_repos", [])
if [sid, repo] not in edges:
edges.append([sid, repo])

def link_session_file(self, sid: str, path: str, tool: str) -> None:
edges = self._data.setdefault("session_files", [])
for edge in edges:
if edge["sid"] == sid and edge["path"] == path:
edge["tool"] = tool
return
edges.append({"sid": sid, "path": path, "tool": tool})

def link_session_ref(self, sid: str, kind: str, value: str) -> None:
edges = self._data.setdefault("session_refs", [])
if [sid, kind, value] not in edges:
edges.append([sid, kind, value])

def sessions_connected(self, kind: str, value: str) -> list[list]:
sessions = self._data.get("sessions", {})

def row(sid: str, via: str) -> list:
props = sessions.get(sid, {})
return [sid, props.get("summary", ""), props.get("updated_at", ""), via,
props.get("agent", "")]

if kind == "file":
hits = [row(e["sid"], e["path"])
for e in self._data.get("session_files", [])
if e["path"].endswith(value)]
elif kind == "repo":
hits = [row(sid, repo)
for sid, repo in self._data.get("session_repos", [])
if repo == value]
else:
hits = [row(sid, f"{k} {v}")
for sid, k, v in self._data.get("session_refs", [])
if v == value or v.endswith(value)]
return sorted(hits, key=lambda r: r[2], reverse=True)

def find_definitions(self, name: str) -> list[list]:
return [
[s["name"], s["kind"], s["path"], s["line"], s["signature"],
Expand Down
Loading
Loading