Skip to content

feat(bundle): keep what an interrupted run wrote, and rebuild an image when a file's permissions change - #71

Merged
earakely-scale merged 2 commits into
mainfrom
edgararakelyan/ledger-pins-modes-orphans
Oct 6, 2026
Merged

earakely-scale merged 2 commits into
mainfrom
edgararakelyan/ledger-pins-modes-orphans

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Three changes to the bundle ledger. Envs written from env.toml will build on them, and agents and file artifacts use them today.

A version an interrupted run wrote is kept

A pending row marked each write until it was recorded, but nothing read it back. If a run was interrupted after its write had landed, while the ledger was re-checking what the write held, the next run found a version it hadn't recorded and wrote it again: the store's latest, v1, wasn't recorded by this bundle. For a built image, that's a rebuild. The re-check hashes the write's files again, so for a large context it takes seconds.

  • The stamp. record() stamps the version on the pending row as soon as the write returns, before the re-check.
  • The keep. check() keeps the stamped version when it's still the store's latest and the inputs hash the same. Ledger.adopt() then records it, with no write.
  • What's still written again: a version someone else wrote since, a change to the inputs, and a write that raised before it returned.
  • The report. The run and the dry run both say v1, unchanged (written by an interrupted run that didn't record it).

A built image's permission bits are inputs

A build copies each file's permission bits into the image, but the ledger hashed contents only, so a chmod alone reused the old image. A file's digest now carries its mode (+755, +700, …) whenever the mode isn't the usual 0644, for built images only.

  • A file with 0644 is hashed as before, so images built only from such files stay reused.
  • An image built before this change from a file with another mode is rebuilt once.
  • A file artifact's mode isn't an input, since nothing it's loaded into keeps it.

One helper for the store refs a write gives no version

plan.unpinned_store_refs(plan, write) returns the version the plan read for each store entity a write names without one. materialize._pinned pins from it, and Ledger.digest hashes it. The ledger now hashes these for every kind whose writer pins, so artifacts as well as envs and agents. No artifact type has store refs today, so no existing digest changes.

Tests

  • Ledger:
    • the helper, the pinned config and the hashed store refs agree for an agent naming a store image without a version;
    • 0755 and then 0700 each rebuild a built image, a 0644 file is hashed as its content alone, and a file artifact's mode isn't an input;
    • a version an interrupted run wrote is kept by the dry run and the run, then recorded, with one version in the store;
    • it's written over once another write lands or a file changes.
  • Materialize: a run interrupted after its write returns; the next dry run and run keep the version and print the line above.
  • Unit tier: 5,862 passed.

End to end

Real runs on macOS arm64 with Docker, from fresh state roots, on the local stores.

check result
two Dockerfile-built agents recorded by main, one with an executable run.sh this branch reuses the other one's image and rebuilds this one's once (files changed: run.sh); both deploy and reply
chmod 700, then 644, then 755 each rebuilds the image, and stat in the image follows: 700, 644, 755
SIGINT once a 1.5 GB file artifact's write had landed (1.74 s in) Aborted!; the pending row holds v1; the next dry run and run keep v1, and the run takes 3.5 s; one version in the store
the same interruption on main the next run writes v2

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no new actionable issue or outstanding previous finding remains.

What we checked:

  • Build digest matches copied files: The ledger hashes files from build_context_files, and the build writer copies those same files with shutil.copy2.

Summary

Bundle runs can keep a version written just before an interruption, and built images now rebuild when their files’ permission modes change. Planned store versions are also used consistently when writes pin and hash unversioned references.

  • A run keeps a matching version written before an interruption.
  • Built images change when a build file’s permissions change.
  • Writes pin and hash the same planned store versions.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Check bundle inputs] --> B{Stamped pending version<br/>is latest and inputs match?}
  B -- Yes --> C[Reuse version]
  C --> D[Record it on a real run]
  B -- No --> E[Write a new version]
  E --> F[Stamp pending row]
  F --> G[Check inputs again]
  G --> H[Record completed write]
Loading

Reviews (2) · Last reviewed commit: "fix(bundle): hash a built image's permis..."

…e when an executable bit changes

Three changes to the bundle ledger, which envs written from env.toml will build on.

A version an interrupted run wrote is kept. The ledger now stamps a write's version on its
pending row as soon as the write returns, before it re-checks what the write held, which can
take seconds. The next run of the bundle keeps that version, rather than writing it again, when
it's still the store's latest and its inputs haven't changed. The run and the dry run report it
as "unchanged (written by an interrupted run that didn't record it)". A version someone else
wrote since is written over, as before.

A built image's executable bits are inputs. A build copies each file's mode into the image, so a
chmod alone now rebuilds it. A file with no executable bit is hashed as before, so the images a
bundle built without one are still reused.

One helper, unpinned_store_refs, names the store refs a write gives no version. Its writer pins
them and the ledger hashes them from the same mapping, for every kind whose writer pins, not only
envs and agents.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner October 6, 2026 17:49
Comment thread src/agent_env/bundle/ledger.py
Comment thread src/agent_env/bundle/ledger.py Outdated
Comment thread src/agent_env/bundle/ledger.py
Comment thread src/agent_env/bundle/ledger.py Outdated
… file is executable

A build copies each file's permission bits into the image, so 0755 to 0700 changes
what runs in it, but both were hashed as executable. A file's mode now joins its
digest whenever it isn't the usual 0644, so images built from such files are
reused as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale earakely-scale changed the title feat(bundle): keep what an interrupted run wrote, and rebuild an image when an executable bit changes feat(bundle): keep what an interrupted run wrote, and rebuild an image when a file's permissions change Oct 6, 2026
@earakely-scale
earakely-scale merged commit 602d730 into main Oct 6, 2026
19 of 20 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/ledger-pins-modes-orphans branch October 6, 2026 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant