mountable.io

Cookbook: clean up one finished agent job

Delete one job's unchanged files by ETag, leave the other job's files in place, and see what a delete does to the bill.

Two agent jobs can share one filesystem. Each job writes under its own prefix in one run directory. Job A is finished.

A recursive delete of that shared directory also removes job B's files.

mountable.io is a persistent filesystem for AI agents. Job A writes a completion manifest that names each file it created and the ETag of that file. Cleanup deletes a listed file only when the path is inside A's prefix and the ETag still matches. It then removes empty directories under that prefix, one directory at a time.

Delete is not recursive. A directory that still has an entry answers 409 directory_not_empty, and the contents stay. A conditional delete sends If-Match with the ETag you expect. See conditional writes.

mountable.io maintains this runnable example.

Setup

Use Python 3.11 or newer and uv. Set MOUNTABLE_API_KEY in your private environment or secret manager. Never put the key in a command argument. Use an existing account with a saved payment method, and a filesystem the key can access. The example creates no account, filesystem or credits.

git clone https://github.com/writeitai/mountable.git
cd mountable/examples/job-cleanup
uv sync --locked
export MOUNTABLE_FILESYSTEM_ID=your-filesystem-id
export RUN="demo-$(date +%s)"

RUN is 1–40 characters: a lowercase letter or digit, then letters, digits or hyphens. Use a new value each time. File operations use the normal usage prices.

Write the two jobs

uv run python cleanup.py write-a --filesystem "$MOUNTABLE_FILESYSTEM_ID" --run "$RUN"
uv run python cleanup.py write-b --filesystem "$MOUNTABLE_FILESYSTEM_ID" --run "$RUN"

Each command is its own process. Job A creates:

  • jobs/$RUN/a/out/report.txt, the text job-a report
  • jobs/$RUN/a/out/notes.txt, the text job-a notes
  • jobs/$RUN/a/complete.json, the paths and ETags of those two files

Job B creates jobs/$RUN/b/keep.txt with the text job-b keep. The manifest does not name it. Both writes use If-None-Match: *, so a second create is refused and the existing bytes stay.

Delete only unchanged files from job A

uv run python cleanup.py cleanup-a --filesystem "$MOUNTABLE_FILESYSTEM_ID" --run "$RUN"

The command reads complete.json and prints one JSON report. For each listed path it checks three things:

  1. The path is inside jobs/$RUN/a and has no . or .. segment.
  2. A directory listing still shows that file, and the file is at most 1 MiB.
  3. The listing's ETag matches the manifest.

A match sends DELETE with If-Match. Anything else stays, and the report says why: etag_changed, outside_allowlist, too_large, not_a_file or delete_failed. Job B's file stays even if a rewritten manifest names it. The check lists the parent directory, so it does not download the file to compare the ETag.

When every listed file is gone, cleanup deletes complete.json with the ETag from that read. If anything was kept, or a delete failed, the manifest stays. partial is then true. You can see which paths were deleted and which remain.

Empty directories under jobs/$RUN/a are deleted one at a time, deepest first. This is not a recursive delete. A directory that still holds a file stays, including a file the manifest never named. cleanup-a does not remove jobs/$RUN/b or the shared jobs/$RUN directory.

uv run python cleanup.py show --filesystem "$MOUNTABLE_FILESYSTEM_ID" --run "$RUN"

After a full cleanup of A, the three A paths print absent and job B's file prints present.

Remove job B when that job is finished

uv run python cleanup.py cleanup-b --filesystem "$MOUNTABLE_FILESYSTEM_ID" --run "$RUN"

This deletes only the known file jobs/$RUN/b/keep.txt, using the ETag from a fresh listing. It then removes jobs/$RUN/b if that directory is empty, and the shared parent only when that is empty too. It does not read A's manifest.

What you are billed

A delete removes the name. It does not walk a tree, and it does not by itself change a spending cap. On the billing page, storage and file count are sampled daily. Deletion takes a final sample, and storage and file-count charges for that content stop after the deletion day. Downloads you already made stay on the bill. retained_bytes can still include the deleted content until reclamation, so the usage figure can lag the delete.

Deleting files does not free this month's peak reservation. Revoking a mount session stops that session. The files stay until something deletes those paths. Keep the session id from the mount and revoke it from the trusted backend when the job ends:

mountable sessions revoke "$SESSION_ID"

At the USD usage prices, storage costs $0.20/GiB-month and downloads cost $0.02/GiB. The files in this recipe are a few hundred bytes. Set a filesystem spending cap when the jobs sit on a filesystem you share with an end user.

Run both jobs, then clean up

uv run python cleanup.py live --filesystem "$MOUNTABLE_FILESYSTEM_ID" --run "$RUN"

live runs the writes and both cleanups as separate processes. It prints b_survived=yes, a_absent=yes and after_revoke_b_survived=yes when job B is still there after the sessions from this run are revoked. It then removes job B. caps_unchanged=yes means the filesystem's spending cap, byte quota, entry quota and mutation rate were the same at the end as at the start. The command does not print the API key.

Offline checks

uv run --locked pytest -q

These checks run the released SDK against a local fake API. They cover job B surviving, a changed ETag, a path outside the prefix, an extra file that keeps its directory, and a failed delete that stops and leaves the rest visible. No cloud calls are made.