← All posts

Implementing the GCC Paper: Giving AI Agents Persistent, Structured Memory

I run AI coding agents across multiple machines, multiple sessions, sometimes for days at a time. The biggest frustration isn't capability — it's amnesia. Every new session starts from zero. The agent

  • ai-agents
  • memory
  • claude-code
  • gcc
  • open-source
  • research-implementation

The 30th re-explanation of the ARCS Health Portal’s architecture to a fresh Claude Code session was the one that broke me. I’d burned 4,000 tokens — words the AI reads and I pay for — describing the same database schema, the same auth flow, the same branch I was on yesterday, to an agent that arrived with zero memory of any of it. Not the bug I’d spent an hour debugging at 2 AM. Not the design decision that took three sessions to settle. Nothing.

Every new agent session starts blank. That’s the design. It’s also the single biggest drain on productivity and budget in AI-assisted coding.

So when I found GCC: Git-Context-Controller by Junde Wu at Oxford, the paper’s framing felt like someone had been reading my terminal logs. Agent memory shouldn’t be a flat text file you dump into a prompt. It should be a version-controlled codebase — branches, commits, merges, and retrieval at different zoom levels, exactly like git. I spent a week building it from scratch. The results surprised me.

Implementing the GCC Paper

Memory as a repo

The GCC paper (arXiv 2508.00031) defines four operations an AI agent can call during its own reasoning — modeled directly on git:

CommandWhen to CallWhat It Does
COMMITAfter a coherent milestoneCheckpoints progress with a three-block narrative summary
BRANCHBefore exploring an alternativeCreates an isolated workspace for experiments
MERGEWhen an experiment succeedsSynthesizes branch results back into the main trajectory
CONTEXTTo orient or resume workRetrieves memory at multiple resolutions

The architecture: agent memory lives in a .GCC/ directory. main.md holds the global roadmap. Each branch gets its own commit.md (milestone summaries), log.md (fine-grained traces of what the agent observed, thought, and did), and metadata.yaml (file structure, dependencies, configs). Agents equipped with GCC hit 48% resolution on SWE-Bench-Lite — the best result published at the time, ahead of 26 competing systems.

What I Built: gcc-memory

gcc-memory is my open-source implementation. ~2,600 lines of Python in four layers:

src/gcc_memory/
├── store.py       # 757 LOC — core storage engine
├── cli.py         # 447 LOC — Typer CLI (commit, branch, merge, context)
├── utils.py       # Atomic writes, file locks, timestamps
├── server.py      # HTTP + WebSocket for real-time streaming
└── adapters.py    # Codex/Claude/OpenCode transcript parsers

integrations/claude/
├── gcc_memory_observe.py   # UserPromptSubmit hook → observations
├── gcc_memory_stop.py      # Stop hook → thoughts
├── gcc_memory_sync.py      # PostToolUse hook → actions
└── hook_common.py          # Shared: debounce, dynamic import, trimming

scripts/
├── backfill_history.py     # Mine 800+ session transcripts into events
└── run_backfill.sh         # uv-backed runner

What a commit remembers

The paper’s signature move is the three-block commit. Every commit captures three things:

  1. Branch Purpose — why this branch exists at all (anchors intent so future sessions know what you were trying to do)
  2. Previous Progress Summary — a compressed chain of all prior summaries on this branch
  3. This Commit’s Contribution — what actually changed in this milestone
### Commit: Implement JWT auth (2026-02-18T10:30:00+00:00 | main)

**Branch Purpose:** Full-stack authentication system

**Previous Progress Summary:** Set up Express server with route structure.
Added PostgreSQL connection pool with migration system.

**This Commit's Contribution:**
Replaced session cookies with JWT tokens. Simplifies the API gateway
and enables stateless horizontal scaling. Validated with integration
tests covering token refresh, expiry, and revocation.

The key mechanism is _synthesize_progress() — it chains previous summaries with a 1,500-character ceiling, so N commits compress into a fixed-size window. After 50 commits, you still get a coherent summary that fits in a few hundred tokens. The memory doesn’t grow with the project.

The three hooks

The paper specifies Observation-Thought-Action (OTA) traces. I capture all three through Claude Code’s hook system — lifecycle triggers that fire at specific moments in every agent session:

HookEvent TypeChannelWhat It Captures
UserPromptSubmitObservationclaude-hookUser’s request (the “what”)
StopThoughtclaude-hookAgent’s reasoning (the “why”)
PostToolUseActionclaude-hookTool execution (the “how”)

The PostToolUse hook is the richest. Instead of just logging “bash” as a tool name, it builds enriched summaries:

# Instead of: "bash"
# We get: "migrate database schema (exit 0)"
def _build_enriched_summary(tool_name, payload):
    if tool_name == "bash":
        desc = tool_input.get("description", "")
        exit_code = result_obj.get("exit_code", "")
        return f"{desc} (exit {exit_code})" if desc else cmd[:120]

Two filters keep logs from drowning in noise: debouncing (a 3-second window merges rapid-fire duplicate events from back-to-back tool calls) and terse-response filtering (skip anything under 60 characters — “Done.”, “OK.”). Together they cut log noise by ~70% while losing almost nothing of value.

The safety-net commit

Every 300 seconds of continuous tool activity, the PostToolUse hook triggers an auto-commit. But that’s the backup plan. The real value comes from agent-driven narrative commits — the skill I wrote explicitly tells the agent: “Auto-commit is a fallback; your narrative commits and curated summaries are what make this memory useful to future sessions.”

The past was harder than the storage engine

The storage engine was straightforward. Making the system useful for projects that already had months of history — that was the real problem.

My first attempt used ~/.codex/history.jsonl, thinking that was the source of truth. It only contains user prompts. No agent reasoning. No tool calls. No record of which files changed. Memory built from prompts alone was nearly useless — like reconstructing a conversation when you only have one side of it.

I almost gave up there.

The breakthrough: Claude Code stores full session transcripts at ~/.claude/projects/{project}/*.jsonl. Each transcript is the complete conversation — user messages, assistant reasoning blocks, and every tool call with its inputs and outputs. I wrote a parser that mines these directly.

the mechanism — hooks, compression, and mining give me the detail

Three hooks, one OTA channel. The implementation wires three Claude Code lifecycle hooks — UserPromptSubmit, Stop, and PostToolUse — to capture the full Observation-Thought-Action trace. Each hook appends a timestamped JSON event to log.md. A 3-second debounce window collapses burst-fire calls (e.g. rapid PostToolUse from a multi-step tool) into one log entry, cutting noise by roughly 70% with negligible information loss.

Commit compression that actually scales. The _synthesize_progress() function chains the text of previous commit summaries up to a 1,500-character cap, then feeds that ceiling into the new commit’s “Previous Progress Summary” block. This means you can have 200 commits and the context cost of loading history stays fixed — it doesn’t grow with project age.

Mining full transcripts, not just prompts. Claude stores complete session transcripts (user turns, assistant reasoning blocks, tool calls) as newline-delimited JSON under ~/.claude/projects/{project-slug}/. The backfill parser reads these directly:

# backfill_history.py — mine full reasoning, not just prompts
for record in records:
    if record["type"] == "user":
        user_texts.append(extract_text(record))
    elif record["type"] == "assistant":
        for block in record["message"]["content"]:
            if block["type"] == "text":
                reasoning_parts.append(block["text"])
            elif block["type"] == "tool_use":
                tool_calls.append(summarize(block))
                if block["name"] in ("Edit", "Write"):
                    files_changed.add(block["input"]["file_path"])

Try it. Point the backfill script at any project slug and watch structured commits materialize from months of history:

python scripts/backfill_history.py --project your-project-slug --output .GCC/

The payoff is immediate: instead of a flat list of user prompts, you get OTA-structured commits where the agent’s reasoning — what it tried, what files it changed, why — is preserved alongside the action.

For the ARCS Health Portal — 36 days of development — the backfill mined 655 Claude sessions and 733 Codex prompts, producing commits like:

2026-01-19 (37 sessions)

[16:37] Implement batch lab upload feature
  Reasoning: Let me start by reading the specification
  Files changed: lab_upload.py, lab_upload_store.py, name_extractor.py
[18:24] Let user mark invalid form history and lab results
  Reasoning: Let me look at the data stores and template
  Files changed: ehr.py, filled_form_store.py, patient_detail.html

Prompts-only was a ghost town. This is a living record.

What the first implementation missed

After the initial implementation, I compared it with the paper. Several pieces were missing:

Paper RequirementInitial StateFix
Git commit on COMMIT/MERGENot implementedAdded --git flag
MERGE calls CONTEXT on target firstMissingAdded context_branch() call before merge
BRANCH initializes commit.mdEmpty fileWrites initial entry with Branch Purpose
main.md has milestones + to-do listOnly Purpose/Decisions/QuestionsAdded Milestones and To-Do sections
Per-file responsibilities in metadataPath list onlyDocumented as optional (paper says “manually added”)

Git integration was the biggest miss. The paper says COMMIT “finalizes the memory and code changes as a Git commit, using the agent-authored summary as the commit message.” Now gcc-memory commit --git stages all changes and creates a real Git commit beside the GCC commit. The agent’s own words become the commit message.

What I would keep

For a single workspace with 1–3 agents, Markdown and YAML with file locks are enough. The storage is simple and auditable. Every event is visible in log.md, every commit is readable in commit.md, and there is no database to migrate or server to keep alive.

Recording observation, thought, and action on every event felt like over-engineering. I almost skipped it. Later, that consistent OTA schema made it possible to reconstruct memories from raw transcripts. Events you do not structure now cannot be reconstructed later.

My first approach auto-generated everything — summaries, highlights, and main.md updates. It was technically correct and useless. It read like a robot summarizing another robot. The better approach was to treat agents as curators: the skill tells them when and how to update main.md, but they write the content.

My biggest wrong turn was using history.jsonl as the source. I spent days convinced it was right. The full session transcripts — conversation, reasoning chain, and file changes — hold the institutional knowledge. A prompt says what was asked. A transcript says what was tried, what failed, and why.

Run it

gcc-memory is open source at github.com/RooseveltAdvisors/gcc-memory

git clone https://github.com/RooseveltAdvisors/gcc-memory
cd gcc-memory && bash install.sh

All credit for the GCC framework goes to Junde Wu’s paper. I just built an implementation and learned a lot along the way.