1. Executive Summary
gh-aw is GitHub's compiler for agentic
workflows: a Markdown file with YAML frontmatter in
.github/workflows/, compiled by a Go binary into a
.lock.yml GitHub Actions workflow that runs a coding agent.
It is on a token-cost list because it meters per-run inference in "AI
Credits" and can cap a run, and that is not why it is in this atlas.
It is here because a workflow run is a session with unusually hard
edges — a fresh container, no filesystem, nothing carried forward — and
gh-aw gives that session three ways to remember things
anyway, and a fourth in private preview. cache-memory puts
a directory in the GitHub Actions cache. repo-memory puts
it on an orphan git branch. comment-memory puts it in a
managed issue or pull-request comment. drive-memory, marked
experimental and gated on enrolment in the GitHub Drives preview, mounts
a named drive over FUSE. All of them materialise as ordinary files under
/tmp/gh-aw/, all are edited by the agent with the file
tools it already has, and all are synced back by a post-agent step the
agent never calls.
The genuinely interesting part is what happens at restore.
cache-memory is not a directory of files; it is a
git repository with one branch per trust level —
merged, approved, unapproved,
none — and a run checks out the branch matching its
workflow's integrity level and then merges down from strictly
higher levels only. The comment in setup_cache_memory_git.sh
states the rule directly: "lower-integrity runs see higher-integrity
data via merge, but higher-integrity runs never see lower-integrity
data." The level is the workflow's own
tools.github.min-integrity guard setting, fixed at compile
time, so it records how much untrusted GitHub content the writing agent
was allowed to read. That is an information-flow lattice applied to
agent memory, and this atlas has very little of it.
Two things narrow it. The level is also the prefix of the cache key,
and the restore keys keep that prefix, so a cache-memory store is only
ever restored at the level that wrote it and the merge has nothing above
it to bring in; the lattice does work only on a store two levels share,
which at this pin is a named drive. And a level is a branch name and a
key prefix, never a field on a file, so the boundary is a partition
chosen by name rather than a predicate on a read. One mark,
negative_eval, on shell tests that assert planted material
is gone after restore; scope_enforced is withheld for that
reason, and the near-miss is in section 9.
The second interesting part follows from the first. Before the agent
is allowed near the restored tree, the same script deletes every
non-sample file under .git/hooks, rebuilds the git metadata
if .git, its config, info or
hooks is a symlink, deletes the .git/info
overrides, unsets every include, includeIf,
credential, alias, filter and
merge config section, points core.hooksPath at
/dev/null, deletes every working-tree symlink, strips the
execute bit from every file, and — when allowed-extensions
is configured — deletes every file whose extension is not on the list.
The threat model is written down in ADR-26587:
"A compromised prior run could therefore plant executable scripts …
which the next agent would encounter without any validation."
Memory is modelled as an artifact written by an attacker who was
you.
Where it is weakest is everything a memory system usually is. There is no retrieval — the store is a directory and the agent greps it. There is no fact, no extraction, no dedupe, no supersession, no contradiction handling and no way to mark anything wrong. The integrity level is a property of the run that wrote the file, never of the claim inside it, so nothing is ever promoted or demoted on evidence. And the reading agent gets the whole directory, bounded only by the 10GB repository cache limit.
2. Mental Model
A memory in gh-aw is a file a previous run of
this workflow left behind. Not a fact, not an embedding, not a
summary — a byte sequence at a path, whose meaning is entirely a
convention between the workflow's Markdown prompt and whatever the agent
decides to write. The compiler validates size, count, extension and
glob, and never parses content itself. What it offers instead is
validation.script: a JavaScript body the workflow author
writes, run under Node.js over the whole memory directory before
persistence, which rejects the save by throwing, returning
false, exiting nonzero, timing out or modifying a file.
Content policy is the author's code or nothing.
So there is no belief lifecycle in the usual sense, and it is worth being precise about what replaces it. A file's status is where it lives, and it has three independent coordinates:
- Which backend. Cache memory is ephemeral by design
(7-day Actions cache retention, 10GB per repository, LRU eviction). Repo
memory is permanent and versioned. Comment memory is a single rendered
document with one current value. Drive memory is durable without
git-branch commits, one active writer per drive, and its ADR-54662 is a
Draftwritten by an ADR-writer agent. - Which integrity branch, for cache memory and drive
memory:
merged>approved>unapproved>none. This is assigned by the workflow's guard setting at compile time, not by the trigger and not by the content, and nothing ever moves a file between branches. There is no promotion path and no adjudication. The lattice governs who may read what, and that is all it does. - Whether it survived the gate. A restored file with a disallowed extension, or a symlink, is deleted before the agent sees it. It was memory; now it is not, and nothing records that it was.
Death is by expiry, eviction, prefix-group pruning in the scheduled maintenance workflow, or overwrite. Nothing is ever marked false. A wrong file stays wrong until an agent happens to overwrite it, and the only record that it was ever different is the git history of the store itself.
Control is hybrid, tilted at the agent. The agent
writes freely with ordinary file edits and never calls a memory tool;
ADR-27479 chose that deliberately over an explicit
comment_memory safe output, on the grounds that an agent
asked to decide when to persist will forget. The human's
control is exercised at compile time, in frontmatter, through limits —
and, for repo memory, afterwards, because the store is a git branch a
person can open a pull request against.
Diagram source
%% caption: a restored cache has its hooks deleted and is fsck'd before use, and merges run strictly down the integrity lattice so lower-trust content cannot escalate
flowchart TD
R["cache restored from a previous run"] --> H["delete .git/hooks/*<br/>core.hooksPath = /dev/null"]
H --> F{"git fsck clean?"}
F -- "no" --> RI["rm -rf .git, reinitialise<br/>working tree preserved"]
F -- "yes" --> CO
RI --> CO["checkout branch = this run's integrity level"]
CO --> M["merge down: strictly higher levels only<br/>-X theirs"]
M --> S["delete symlinks · chmod a-x ·<br/>drop disallowed extensions"]
S --> A["agent reads and edits files"]
A --> C["commit run-$GITHUB_RUN_ID<br/>on the same integrity branch"]
C --> SV["save cache"]
L["legacy flat files, trust unknown"] -.->|"committed to 'none' only,<br/>to prevent trust escalation"| CO3. Architecture
gh-aw is a compiler and a CLI, not a
runtime. gh aw compile reads a Markdown workflow, resolves
imports, and emits a .lock.yml GitHub Actions workflow
containing every step inline. There is no gh-aw process at
run time: what executes is GitHub Actions running generated YAML that
shells out to scripts vendored from actions/setup/sh/.
The memory subsystem is therefore compile-time code that emits run-time steps:
| Component | Where |
|---|---|
| Cache-memory config, key derivation, integrity-aware keys | pkg/workflow/cache_memory.go,
pkg/workflow/cache_integrity.go |
| Drive-memory config, mounts and commit steps (experimental) | pkg/workflow/drive_memory.go,
pkg/workflow/drive_memory_config.go |
| Author-supplied validation and persistence eligibility | pkg/workflow/memory_validation_config.go,
actions/setup/js/memory_custom_validation.cjs,
actions/setup/js/memory_file_eligibility.cjs |
| Repo-memory config, validation, branch naming | pkg/workflow/repo_memory.go,
pkg/workflow/repo_memory_validation.go |
| Repo-memory prompt section | pkg/workflow/repo_memory_prompt.go |
| Comment-memory safe-output config | pkg/workflow/comment_memory.go |
| Pre-agent restore, lattice and sanitisation | actions/setup/sh/setup_cache_memory_git.sh |
| Post-agent commit | actions/setup/sh/commit_cache_memory_git.sh |
| Store repair | actions/setup/sh/check_cache_memory_git_integrity.sh |
| Repo-memory clone, git-state hardening and filename hygiene | actions/setup/sh/clone_repo_memory_branch.sh,
sanitize_repo_memory_filenames.sh |
| Repo-memory push and concurrent-merge policy | actions/setup/js/push_repo_memory.cjs |
Persistence is entirely GitHub's: the Actions cache service, the git
object store, and the issues API. There is no database, no vector index
and no search service, because there is no search. Repo-memory commits
go through the GraphQL createCommitOnBranch mutation, which
makes them Verified under GitHub's own GPG key and lets
them satisfy a ruleset that requires signed commits — with a documented
hole: the mutation cannot express symlinks, executable bits or
submodules, so an artifact containing one falls back to a plain
git push that a signed-commit ruleset will then reject.
Deployment and ergonomics
Nothing to stand up, and nothing that runs locally. Adopting this
means adopting GitHub Actions, an agent engine with credentials
configured as repository secrets, and the gh aw extension
for compiling and for reading logs. The cost is a GitHub-shaped lock-in
rather than an infrastructure one: every durable surface here is a
GitHub product, and the design has no meaning off the platform.
The store is as human-readable as it gets. Repo memory is a branch
you can check out; comment memory is a comment you can read in a
browser; cache memory is a git repository whose log is one commit per
run, named run-<GITHUB_RUN_ID>. Repair by hand is a
git push.
4. Essential Implementation Paths
Restore and read.
generateCacheMemoryGitSetupStep in
pkg/workflow/cache_memory.go emits a step running
actions/setup/sh/setup_cache_memory_git.sh with
GH_AW_CACHE_DIR, GH_AW_MIN_INTEGRITY and —
only when configured — GH_AW_ALLOWED_EXTENSIONS as a
colon-separated list. The script detects a cache hit by the presence of
.git, flattens a legacy nested layout, reinitialises if the
git metadata is symlinked, deletes hook files, runs
git fsck --connectivity-only and reinitialises on
corruption while preserving the working tree, scrubs the git config and
.git/info, checks out the integrity branch, merges down,
then sanitises. Drive memory emits the same script against its
mount.
Context assembly.
buildRepoMemoryPromptSection in
pkg/workflow/repo_memory_prompt.go returns a
PromptSection pointing at a template file, substituting
GH_AW_MEMORY_DIR, GH_AW_MEMORY_DESCRIPTION,
GH_AW_MEMORY_BRANCH_NAME and a
**Constraints:** block listing the allowed globs and the
size, count and patch-size caps. This is the whole of retrieval: the
agent is told a path and the rules, and goes looking. Comment memory is
referenced the same way. ADR-27479 says its content is injected into the
prompt, but injectCommentMemoryPrompt writes only a list of
file paths
(actions/setup/js/setup_comment_memory_files.cjs:177-192),
and the threat-detection prompt receives each file's path and size.
Write and persist. The agent edits files in the
mounted directory with whatever editing tools its engine has. Afterwards
commit_cache_memory_git.sh stages everything with
git add -A and commits run-${GITHUB_RUN_ID} on
the current integrity branch with --allow-empty, so the log
has a row per run whether or not anything changed, then
git gc --auto. Repo memory instead filters by
file-glob and allowed-extensions — a file that
fails either is ignored, never uploaded, counted or pushed, rather than
failing the run — then validates max-file-size,
max-file-count and max-patch-size, runs the
author's validation.script if there is one (in the agent
job and again in the push job), and pushes only if threat detection
passes; a push_repo_memory safe-output tool exists so a
workflow can fail on the limits early rather than at the end.
Update, delete, forget. There is no delete path.
Removing a file from the working tree is picked up by
git add -A and committed as a deletion; expiry is the
Actions cache's own 7-day retention and LRU; the scheduled Agentic
Maintenance workflow groups cache entries by key prefix (everything
before the run ID) and keeps only the newest per group.
Scope. Two layers, and both select a whole store by
name. The integrity branch, above. And the cache key:
computeIntegrityCacheKey prefixes every key with
memory-{level}-{policyHash}-
(pkg/workflow/cache_integrity.go:163-179), and
buildCacheRestoreKeys strips only the run id, plus the
workflow id for scope: repo, so the prefix survives into
every restore key (pkg/workflow/cache_steps.go:111-133).
Editing any policy field forces a miss rather than reusing memory
gathered under the old policy. The level is the frontmatter enum
tools.github.min-integrity, defaulting to none
(pkg/workflow/tools_parser.go:312-313,
cache_integrity.go:141-146), emitted as a literal
GH_AW_MIN_INTEGRITY
(pkg/workflow/cache_memory.go:120). A drive is mounted by
drive-name alone
(pkg/workflow/drive_memory.go:33), so it is the one store
where two levels meet and the merge-down has work to do.
5. Memory Data Model
There is no schema. The unit is a file; the system's model of it is
(path, size, extension).
Scoping is real and multi-axis, which is unusual for a system with no data model at all:
- Integrity level — a git branch and a cache-key prefix: read-down across the branches of one repository, no sharing at all across cache keys, and no field on any file.
- Repository and branch — Actions cache scoping is branch-local with fallback to the default branch, which the documentation calls out as a behaviour to plan around: on a non-default branch the first restore usually comes from the default branch, and later saves start a branch-local lineage.
- Named store —
idselects/tmp/gh-aw/cache-memory-{id}/or/tmp/gh-aw/repo-memory-{id}/, and repo memory mapsidto branch{branch-prefix}/{id}. - Target repository —
target-reposends repo memory somewhere else entirely, which is the documented way to isolate memory from the repository being worked on.
Provenance is the git log of the store: one commit per run, named for
the run ID, on the branch of that run's integrity level. There are no
temporal fields, no validity interval, no version chain at the level of
a claim, no TTL a user can set, and no pinning.
cache-hit-history.json is written on a hit with
run_id, timestamp and cache_files
— a restore receipt, not a record of what the memory says.
Nothing separates episodic from semantic material. The workflow author does that by choosing filenames, or does not.
6. Retrieval Mechanics
There is no retrieval mechanism, and that is a design
position rather than an omission. The store is mounted as a
directory and the agent uses its own Read,
Grep and Glob. No embedding is computed, no
index is built, no ranking, no fusion, no reranking, no token budget on
the read side.
The consequences are the ones you would predict, plus one you might not:
- Cost scales with what the agent chooses to open, not with what the system decides to inject — which is cheaper than eager injection for a large store and worse for a small one, since a small store would have been better simply pasted.
- Relevance is the model's problem. A file that stops being true stays exactly as discoverable as one that is.
allowed-extensionsis, incidentally, the only content-shaped filter on the read path, and it filters by file extension.
The one non-obvious consequence is that the restore gate is
the retrieval policy. What a run can see is decided entirely by
which cache key restores, which branch is checked out and which way the
merge runs, before the agent runs. That is a partition, not a filter: no
file carries a level and no read consults one. The agent cannot phrase
its way around it, and the price is that it moves only whole stores. The
.git directory sits inside the mounted tree with every
branch's objects, so on a drive holding several levels a general shell
can read a lower branch; the rubric does not count a general shell
against a scope.
7. Write Mechanics
Writes are in-band file edits, invisible to the persistence layer until the run ends. There is no extraction model, no LLM in the write path, no dedupe and no consolidation. Whatever the agent leaves in the directory is what gets stored.
Filtering is structural and happens at two moments. On restore:
symlinks deleted, execute bits stripped, disallowed extensions removed.
On push, for repo memory: file-glob, where a slashless
pattern like *.json matches only files at the artifact root
(depth 0) and **/*.json is needed for subfolders; the
reference page gives that rule, while globPatternToRegex
documents a matchSubfolderRoot option with the opposite,
depth-1-only meaning that no caller passes. Then
max-file-size (100KB default), max-file-count
(100 default), max-patch-size (10KB default, 1MB ceiling),
then threat detection. format-json: true pretty-prints
.json before commit, which is a diff-quality decision
rather than a memory one and reads as a sign the maintainers expect
humans to review these branches.
Conflict handling is stated plainly and is the weakest link in the
write path: concurrent pushes are replayed onto the latest remote state
and your file changes win. Two workflows writing the
same memory file concurrently do not merge; the later one erases the
earlier. The one exception is .jsonl: the push step sets a
checkout-local *.jsonl merge=union attribute and pulls with
-X ours, so conflicting JSONL regions keep rows from both
sides. A union merge produces a merge commit, which the signed
createCommitOnBranch path cannot express, so on a branch
whose ruleset requires signed commits that retry falls back to a push
the ruleset rejects. Drive memory takes the opposite position: one
writer lease per drive, and a digest check that fails the save rather
than overwrite a drive that changed during threat detection.
Operational cost
The write path is fully deferred and costs the agent nothing. No LLM call, no blocking, no round trip — an edit is a file write, and persistence happens in a separate job after the agent has exited.
The lag before a memory is retrievable is one workflow run, and the honest number is longer than that: a cache saved at the end of run N is available to run N+1 only if the key matches and the entry has not been evicted, and on a non-default branch the first restore commonly comes from the default branch instead. Nothing here is available mid-run to a concurrent run.
No background pass rewrites the store. git gc --auto
runs after each commit and the maintenance workflow prunes cache entries
by key prefix on a schedule; neither reads content, so neither has a
token bill.
On the read side there is no per-turn injection to bound, which sidesteps cache-preserving injection entirely rather than solving it: the prompt carries a path and a constraints block, both fixed for the run, and the volatile material arrives as tool results. Comment memory adds one path per memory id to that block, not the content, so its fixed cost grows with the number of ids rather than their size.
8. Agent Integration
The integration surface is YAML frontmatter, and nothing else:
tools:
cache-memory: true
repo-memory:
branch-name: memory/insights
file-glob: ["*.md", "*.json"]
max-file-size: 1048576
There is no MCP server for memory, no SDK and no REST endpoint. The compiler turns those keys into steps; the agent is told a directory. This is the lowest-ceremony agent integration in this atlas — an agent that can read and write files already supports it, and porting the idea to another CI system is a week of shell, not a library adoption.
The agency split is worth naming. The agent has total freedom over content and zero awareness of persistence. It cannot choose to save, cannot choose not to save, cannot address a memory by id, and cannot ask what it remembers — it can only look. The engine's own compaction and session lifecycle are irrelevant here, because a run ends and the container is destroyed regardless.
9. Reliability, Safety, and Trust
This is the strongest section of the design, and it is strong in an unusual direction: it protects the machine from the memory, not the memory from the world.
What is defended:
- Execution planted in the store. Hook files under
.git/hookssurvive in the cache but are untracked, so a prior run could write apost-checkouthook that fires on the host runner before any sandbox exists. The script deletes them and setscore.hooksPathto/dev/nulltwice, before and after the format check. - Symlink escape. All working-tree symlinks are
deleted, with the reason written in the script: a link out of the cache
directory would bypass the regular-file checks that follow. A symlinked
.git,.git/config,.git/infoor.git/hookscauses the metadata to be rebuilt before any git command runs. - Git configuration planted in the store.
include,includeIf,credential,alias,filterandmergesections andcore.fsmonitor,core.sshCommandandcore.attributesFileare unset on restore for cache and drive memory and on clone for repo memory, so a prior run cannot turn the next run'sgit mergeinto command execution. - Credentials in the store. The repo-memory clone
authenticates with a transient
http.extraheaderpassed throughGIT_CONFIG_*environment variables and setsoriginto a token-free URL, so the job token is not written into the memory directory's.git/config. - Unexpected file types and executables, per
ADR-26587, unconditionally and at every integrity level including
none. - Trust escalation. Legacy flat files from an older
gh-aware committed to thenonebranch only, because their provenance is unknown. - Policy drift. Changing the guard policy changes the cache key, so memory gathered under a looser policy is not silently reused under a stricter one.
- Store corruption.
git fsckon restore, with reinitialisation that keeps the working tree, plus a separate integrity-check script that reseeds.
What is not defended, and this is where the shape of the thing shows:
- False content. Nothing checks whether a remembered
claim is true, and there is no mechanism that could express that it is
not. A prompt-injected fact written into
notes.mdby an unapproved run is, from every later unapproved run's point of view, simply what it knows. The lattice bounds the blast radius by trust level; it does nothing within a level. - The trust label is about the writer, not the
belief.
mergedsays a merged commit produced this file. It does not say anyone read it. - Concurrency. Last writer wins, by documented design.
- Secrets. The repo-memory documentation says not to
store sensitive data and offers
target-repofor isolation; nothing enforces it, and a memory branch on a public repository is public. - Who wrote a comment memory. The restore lists every
comment on the target thread and turns each fenced
gh-aw-comment-memory:<id>block into a file, a later comment overriding an earlier one with the same id (actions/setup/js/setup_comment_memory_files.cjs:101-124). The writer updates a comment only if it carries the<!-- gh-aw-agentic-workflow:provenance marker (actions/setup/js/comment_memory.cjs:109-117); this read checks neither that marker nor the comment's author, and comment memory has no integrity level at all.
Capability marks:
negative_eval— awarded; section 10.scope_enforced— withheld. The level is a branch name and a cache-key prefix taken from the workflow's compile-time guard setting; the run checks out its branch and merges higher ones (actions/setup/sh/setup_cache_memory_git.sh:241,:262-283), and no file carries a level that a read applies as a predicate. That is a physical partition selected by name, which the rubric does not count.tombstone,trust_state— nothing can mark a file wrong; the integrity level describes the writer's guard policy and no read filters a file on a status.bitemporal— no temporal field;cache-hit-history.jsonis a restore receipt.audit_log— one commit per run on the store's git log is history, not a record of memory mutations.human_review— nothing waits; repo memory can be corrected by a pull request after the push has landed.
10. Tests, Evals, and Benchmarks
Test coverage of the memory subsystem is substantial by this atlas's
standards and mostly compiler-shaped. Seventeen test
files in pkg/workflow/ carry memory in the
name, holding 130 func Test entries, and they assert what
the compiler emits: that restore precedes execution, that
restore keys strip the run ID so a run can fall back to an earlier one,
that GH_AW_ALLOWED_EXTENSIONS is emitted only when
configured, that multiple named caches produce distinct directories,
that import merge rules resolve local-wins, that repo-memory paths stay
consistent between the prompt and the steps.
setup_cache_memory_git_test.sh,
check_cache_memory_git_integrity_test.sh,
commit_cache_memory_git_test.sh and
clone_repo_memory_branch_test.sh exercise the shell
directly.
The shell suite is where the negative cases are, and they earn
negative_eval. Test 4 restores data.json,
notes.md, helper.sh and
archive.zip under an allow-list of .json:.md
and asserts the two scripts and archives are gone and the two data files
remain; Test 3 is the unfiltered control in which all four survive; Test
10 plants a symlink to /etc/passwd and asserts it is
removed while the real file stays. Test 2b writes an attacker's
.git/config — an fsmonitor hook, a credential
helper, an alias, a smudge filter, a merge driver — and asserts every
section is gone after restore. These are assertions that specific
material does not reach what the agent reads, and they are about the
store's capabilities, not its claims: nothing asserts that any
content is kept from a run.
What is not tested, as far as the tree shows, is the property the
design is built on. There is no test in which a file written on the
none branch is shown to be invisible to a run at
merged — the level the lattice exists to protect. The
integrity tests assert the env-var contract that selects a branch; the
read-down guarantee is enforced by the merge loop and asserted nowhere.
That is the test I would want before trusting this, and it is a cheap
one: two runs and a git show.
There is no retrieval-quality evaluation, which is consistent — there
is no retrieval. There is no benchmark and no paper; the README, the
reference docs and a large docs/adr/ tree are the design
record, and the ADRs are unusually good, carrying rejected alternatives
and negative consequences rather than only the decision.
I ran nothing. Every claim here comes from reading the tree at
9259aea1….
11. For Your Own Build
Steal
- Give memory an integrity level and enforce
read-down. If some of your sessions are more trusted than
others — an authenticated user versus a public demo, an internal ticket
versus a fork PR, a reviewed document versus a scraped one — then a
lattice where low reads high and high never reads low is a containment
property you can implement with a scope column and one filter. It is
much weaker than verifying content and much cheaper, and the two
compose.
gh-awpartitions by branch and cache key instead, which moves whole stores and gives a single store holding two levels nothing to filter on. - Treat your own store as an untrusted input at load. The question "what could a compromised earlier session have written here, and what would happen when I load it" has an answer for most memory systems, and the answer is rarely "nothing". Deserialising a pickle, restoring a directory, evaluating a template, reading a path — all of it. Strip the capability, do not detect the attack.
- Make the store a git repository when the store is
files. One commit per session, named for the session, buys a
free history, a free diff, free corruption detection via
fsck, and hand repair with tools the operator already has. Note the atlas's own line here: git history is not an audit log of memory mutations, because it records the state of a tree rather than the events that changed a belief. It is still worth having. - Separate the durability tiers explicitly. Ephemeral-and-fast versus permanent-and-versioned as two named backends with one interface, chosen per store rather than globally, is a cleaner answer than one store with a TTL field.
Avoid
- Do not let "last writer wins" be the concurrency answer for memory. It is fine for a cache and wrong for a store the agent reasons from, because the loss is silent and the losing content is exactly the content another agent thought worth writing.
- Do not confuse a provenance label with an epistemic one. Knowing a fact came from a trusted context is not knowing it is true, and a lattice can make a system feel governed while nothing in it can ever be marked wrong.
- Do not ship a store with no delete semantics beyond overwrite. Correcting a wrong memory here requires an agent to notice it, which requires it to be read, which is the failure you are trying to fix.
Fit
Take this if your agent's sessions are already CI jobs; almost nothing here transfers otherwise, and the parts that do transfer are ideas rather than code. The design assumes a session is short, isolated, untrusted by default, and followed by a step you control — which is true of a workflow run and false of a chat.
Walk away if you need the agent to reason about what it
remembers rather than read it. There is no fact, no confidence, no
contradiction and no correction, and adding them would mean building a
memory system next to this one rather than extending it. The right
reading of gh-aw for most people is as a security model for
memory that happens to ship with a filesystem attached.
12. Open Questions
- Does the read-down guarantee actually hold end to end? The merge
loop looks correct, but nothing in the tree demonstrates a
none-branch file failing to reach amergedrun, and the branch is also the thing a corrupted-cache reinitialisation resets. - What happens to the lattice when the cache is cold? A reinitialised repository starts every branch from the same empty commit, so the first run at each level sees nothing — but the interaction between branch-scoped Actions caches, the default-branch fallback and integrity branches is not something reading can settle.
- How large do these stores get in practice, and does the agent's own
reading of a grown directory become the dominant cost?
gh aw logsreports AIC per run, so the data exists in users' accounts; nothing is committed here. - Does threat detection inspect memory content on the way out, and what does it reject? The wiring is asserted in tests; the detector's own criteria were not traced in this read.
- Does the lattice mean the same thing on drive memory? The drive reuses the cache-memory script and its integrity branches, but a drive is a single named mount with one writer lease that the ADR calls repository-wide and branch-aware, rather than an Actions cache keyed per branch and policy.
Appendix: File Index
Compile-time configuration
pkg/workflow/cache_memory.go ·
pkg/workflow/cache_integrity.go ·
pkg/workflow/drive_memory.go ·
pkg/workflow/drive_memory_config.go ·
pkg/workflow/memory_validation_config.go ·
pkg/workflow/cache_validation.go ·
pkg/workflow/repo_memory.go ·
pkg/workflow/repo_memory_validation.go ·
pkg/workflow/comment_memory.go ·
pkg/workflow/compiler_custom_job_memory.go
Context assembly
pkg/workflow/repo_memory_prompt.go
Run-time steps
actions/setup/sh/setup_cache_memory_git.sh ·
actions/setup/sh/commit_cache_memory_git.sh ·
actions/setup/sh/check_cache_memory_git_integrity.sh ·
actions/setup/sh/clone_repo_memory_branch.sh ·
actions/setup/sh/create_cache_memory_dir.sh ·
actions/setup/sh/sanitize_repo_memory_filenames.sh ·
actions/setup/js/push_repo_memory.cjs ·
actions/setup/js/memory_custom_validation.cjs ·
actions/setup/js/memory_file_eligibility.cjs
Design record
docs/adr/26587-pre-agent-cache-memory-working-tree-sanitization.md
·
docs/adr/27479-comment-memory-file-based-agent-memory-with-github-persistence.md
·
docs/adr/44037-restore-memory-read-only-access-custom-jobs.md
·
docs/adr/44015-expose-memory-stores-to-on-steps-pre-activation.md
· docs/adr/54662-add-drive-backed-workflow-memory.md ·
docs/src/content/docs/reference/cache-memory.md ·
docs/src/content/docs/reference/repo-memory.md
Tests
pkg/workflow/cache_memory_integration_test.go ·
pkg/workflow/cache_memory_restore_keys_test.go ·
pkg/workflow/cache_memory_threat_detection_test.go ·
pkg/workflow/cache_integrity_test.go ·
pkg/workflow/repo_memory_path_consistency_test.go ·
actions/setup/sh/setup_cache_memory_git_test.sh ·
actions/setup/sh/clone_repo_memory_branch_test.sh
History
2026-10-01 — 9259aea1…
— audited at the same commit; scope_enforced withdrawn. The
integrity level is a git branch and a cache-key prefix, taken from the
workflow's compile-time min-integrity setting; no memory
file carries it and no read applies it as a predicate, so the boundary
is a physical partition, which the rubric does not count. Two claims
were also wrong. The level is fixed per workflow, not set by the
trigger, and the restore keys keep the level prefix, so a cache-memory
store is never shared across levels and the read-down merge works only
on a drive two levels share. Comment memory reaches the prompt as file
paths, not content. Newly recorded in section 9: the comment-memory
restore reads every comment on the thread with no author or provenance
check. One mark.
2026-09-15 — 9259aea1…
— 1,565 commits on, 2026-09-14, read from a depth-1 clone of each
commit. Screened before reading: 3 auto-run surfaces, 1 build-time
execution point, 6 unpinned surfaces and 13 dependency surfaces inside
the seven-day cooldown; nothing was executed or installed.
negative_eval is added, and it was missed rather than new:
setup_cache_memory_git_test.sh at the previous pin already
asserted that a disallowed helper.sh and a planted symlink
are removed from the restored tree beside kept files and an unfiltered
control. Since the pin: an experimental drive-memory
backend on the GitHub Drives preview that reuses the cache-memory git
script and integrity branches; an author-supplied
validation.script that can reject a save on content;
git-config and .git/info scrubbing and symlinked-metadata
rejection on restore and clone; a repo-memory clone that no longer
writes x-access-token:${GH_TOKEN}@ into the memory
directory's .git/config, which it did at the previous pin;
a JSONL union merge on concurrent repo-memory conflicts; and a reversal
of the slashless file-glob rule from depth 1 to depth 0,
with non-matching files ignored instead of failing the run.
cache.go became cache_memory.go. The read-down
property of the lattice is still asserted by no test, and
scope_enforced stands on the merge loop as before.
2026-08-09 — c9dca3e2…
— first reading, from the awesome-ai-tokenomics
triage. Screened before reading: 3 auto-run surfaces
(.devcontainer/devcontainer.json, whose
postCreateCommand pipes the project's own
install-gh-aw.sh from
raw.githubusercontent.com into bash;
.gitattributes, whose three filter drivers are git-lfs on
slide assets; .vscode/settings.json, which sets only
Copilot and search options), 6 dependency surfaces inside the seven-day
cooldown and 5 unpinned manifests. Nothing was executed and nothing was
installed.