Status: experimental C9-G implementation contract
Issue: #597 (slice C9-G), parent #570
Authority: durable storage only; no provider dispatch, verification, acceptance, application-branch write, integration, merge, repository administration, or secret-management authority.
C9-A through C9-F define the durable record, canonical serialization, local compare-and-set behavior, idempotent admission, reconstruction, and killed-run recovery. C9-G supplies the missing GitHub-native storage adapter so those records survive hosted-runner teardown without writing the protected application branch.
The implementation is:
idkmesh/github_ledger_github_store.py
It stores compact C9 records through GitHub’s Git Data API on one dedicated branch. It does not call a worker/provider and it never interprets a stored record as permission to execute, accept, push application code, integrate, or merge.
The default storage branch is:
idkmesh-ledger
The branch is created as an orphan/root commit containing only the store marker. It has no parent in the application history.
The adapter refuses to use:
That is a structural safety boundary: normal C9 state changes do not require a
commit to main, a release branch, or another application branch.
The branch marker is:
.idkmesh-ledger/store.json
and binds the storage schema version, repository, and ledger branch. A missing, non-canonical, or mismatched marker fails closed.
All durable paths are generated by the adapter:
.idkmesh-ledger/store.json
admissions/
<sha256(dispatch_key)>.json
runs/
<sha256(run_id)>/
records/
00000000000000000001.json
00000000000000000002.json
...
Raw dispatch_key and run_id values are retained inside their canonical
documents but are not interpolated into path segments.
The dedicated branch accepts only the marker, C9 admission documents, and C9 record paths. Any unexpected blob fails closed rather than silently becoming part of canonical ledger state.
If the ledger branch does not exist:
refs/heads/idkmesh-ledger.If another initializer wins the create-ref race, the loser simply re-reads the branch and validates the winner’s marker.
The adapter does not seed the ledger from main; this keeps application code
and operational ledger history independent.
For every mutation the adapter:
force=false.A stale candidate commit is therefore non-fast-forward after another writer wins. The transport maps GitHub conflict/validation responses from this fast-forward-only update into a conflict, then the store re-reads and rebuilds within a bounded retry count.
The default retry budget is eight attempts. Exhaustion fails closed.
This is the remote equivalent of C9-C’s local git update-ref <new> <old>
proof. The transport has a different API surface, but the invariant is the
same: a writer must extend the head it observed rather than replacing a newer
winner.
C9-G deliberately uses one repository-level ledger branch rather than one GitHub branch per run. That gives GitHub one normal branch/ref to retain and keeps the application branches untouched.
The consequence is that two independent runs can contend on the same ledger branch. Contention is handled by the same bounded re-read/rebuild protocol. This is acceptable for the G0 repository-first profile; a future storage profile may shard or move operational state when measured size/contention justifies it.
append_with_retry(run_id, build) reads the latest run chain from the current
branch snapshot and invokes the builder against that exact state.
The next record must:
An exact replay of an already-stored record_id with the same canonical
digest returns a no-op. Reuse of the same record_id with different content
fails closed.
For callers that already hold one fixed record,
append_record(record) is the convenience wrapper. Callers producing a new
record under concurrency should use the builder form so the record is rebuilt
after a lost branch race.
reserve_admission() stores the same C9-D admission document at the
hash-derived admission path.
Semantics stay identical to C9-D:
same dispatch_key + same request_digest
-> return the first admission
same dispatch_key + different request_digest
-> AdmissionConflict
same dispatch_key + same digest + another proposed run
-> return the run admitted first; do not open a second run
admit_dispatch(first_record) first reserves/adopts the admission and then
ensures the winning run has its sequence-1 record. A replay can therefore close
the crash gap between admission persistence and first-record persistence.
The adapter records admission; it does not submit work to a provider.
read_run_records(run_id) loads canonical record blobs from the dedicated
branch and verifies the complete chain.
recover_run(run_id) composes that durable read with C9-E
reconstruct_run_state().
This means a fresh coordinator does not need the original Actions runner’s filesystem or process memory to reconstruct the recorded run state.
Provider reconciliation and next-step execution remain governed by C9-F and their independent authority gates.
GitHubRestGitDataTransport is a bounded standard-library Git Data transport.
It:
https://api.github.com;parents list;refs/heads/... ref explicitly;{"force": false}.A 409/422 while creating/updating the expected ref is handled conservatively as a storage conflict. The store re-reads instead of overwriting.
The adapter needs repository content/Git-data write capability sufficient to create Git objects and update the dedicated ledger branch. It does not need or exercise:
Repository governance must separately restrict who/workflows receive the token. Possessing a token that can write the ledger does not grant IDKMesh dispatch, verification, acceptance, or merge authority.
A writer can create a blob/tree/commit and then lose the final branch-update race. Those Git objects are not canonical ledger state because the dedicated branch never points to them.
Canonical C9-G state is exactly the history reachable from the validated ledger-branch ref.
Garbage collection of unreachable objects is a repository/provider concern, not a signal that a failed writer succeeded.
Focused tests cover:
force=false;The tests use an in-memory Git-object transport for deterministic race behavior and a mocked HTTP opener for the REST boundary. They make no live mutation to a GitHub ledger branch.
C9-G establishes where essential metadata can live durably and how concurrent writers preserve it.
C9-H still owns the retention/reference policy, including:
Until C9-H lands, C9-G does not delete or compact canonical ledger records.
C9-G does not: