Skip to content

Remove an idle conversation's landed files, and keep its structure in skeletons - #65

Merged
wu-sheng merged 2 commits into
mainfrom
retention-skeletons
Oct 13, 2026
Merged

wu-sheng merged 2 commits into
mainfrom
retention-skeletons

Conversation

@wu-sheng

Copy link
Copy Markdown
Member

Step 4 of retention. A storage root no longer only grows.

What it does

storage.retention, -1 and off by default, removes the landed files of a conversation that was sent and has had nothing new land for that long, such as 72h or 30d. asz collect and asz server do it at the end of a pass, after the send.

A conversation's files go together, and only when all of these hold:

  • It has an owner in push.state: it was sent, or a send was tried.
  • Nothing landed for it within the retention.
  • Its head round covers its newest file, and its parse did not fail this pass.
  • With metrics derived, every file of it is derived.
  • It holds no provider bodies. Exact provider cutting after a removal comes in step 5.

Sending is not waited for. Retention never runs over a scenario root.

Skeletons

Each removed file is replaced by <name>.sk: Session Data, compressed with gzip. It keeps every record and part in its place, with every identifier the index reads, and no content.

  • A changes record keeps its join, its time, and each file's path, operation, sides and line counts, with no diff.
  • An execution record keeps its join, its time and its outcome.
  • A provider body keeps its manifest without segments, and every piece's digest.

The header's new removed object records:

  • the file's digest, size and record count;
  • when, and by which removal, it was removed;
  • what the receiver confirmed of it, and the owner.

The index rebuilt from skeletons is the index rebuilt from the landed files, byte for byte, so a conversation that becomes active again continues the same chain. A skeleton is never sent.

Measured on 2026-10-13 on one root: 7,235 landed files of 847.5 MB had skeletons of 29.6 MB, 3.5%.

Nothing lost

  1. removing.json records the removal: its generation, the newest sequence it removes, and its phase.
  2. Each skeleton is written and synced, and the error of its first sync is checked. It takes its file's modification time and is synced again. A failed sync stops the removal, and the next pass writes every skeleton again.
  3. Each landed file is deleted only once its skeleton reads whole and records the file's digest. Then each metrics request the receiver has goes; a request not yet sent goes on the first pass after it is. A receipt stays, so a deriver whose state a power loss took back derives the file as before. Every directory of the removed range is synced.
  4. removing.json is deleted.

Every pass finishes an unfinished removal before anything lands. No collector lands into a session while its removal is unfinished: it finds the session busy.

Readers

Reader What it does with a skeleton
index, parse reads it as the landed file; binds a round to the digest it recorded
metrics keys and digests by the landed path; tokens from a skeleton, no MCP call
export never sends it
LangSmith continues its next position from it
asz verify checks lines and bytes through it; reports removed files apart, never as verified
page marks the file and its steps removed; follows a removal mid-read; keeps what it read for the chain head and the removal generation in removal.state, so a separate asz view sees a removal
asz show says the record was removed
asz repack refuses a session with skeletons

Also in this PR: the first commit takes out the owner adoption for push.state files an older build wrote. Upgrades from older builds are not supported.

Deferred to later steps:

  • Step 5: provider body sessions.
  • Step 6: the page's banner.
  • Step 7: local mode, for never-sent sessions.
  • Step 8: sent_state_retention and the retired marker.

Tests

Each was proven by reverting its fix.

  • tests/chain/retention_test.go
    • All 34 scenarios run twice, once removing everything after each send. They must give the same sent files, rounds and metrics, chains, indexes (also rebuilt from skeletons only), page file lists, changes and executions, and verify cleanly.
    • Each reason a session is kept.
    • Receiver evidence and generations.
    • Metrics cleanup: a request sent later goes, and receipts stay.
    • A crash at every point of a removal.
    • A failed first sync, and a failed sync after the deletions.
    • Nothing lands during a removal.
    • Empty and wrong skeletons.
    • Metrics derived from skeletons.
    • An untimed file stamped after a removal.
    • The page following a removal, from another process too, and keeping nothing while one is in progress.
  • Unit tests:
    • pkg/skeleton: the format.
    • internal/storage: listing, digests, the counter, the intent, the removal generation, the fallback open, the busy lock.
    • internal/verify: removed files reported, not verified.
    • internal/view: whole-record steps, removed bodies not served.
    • internal/config: parsing and overflow.
    • internal/adapters/langsmith: the next position.
    • cmd/asz: the pass line, scenario roots left alone, repack refusing, and show and verify.

Upgrades from older builds are not supported, so a session such a
build sent takes its owner at its next send, like any other session.
@wu-sheng wu-sheng added this to the 0.6.0 milestone Oct 13, 2026
… skeletons

storage.retention, -1 and off by default, removes the landed files of a
conversation that was sent and has had nothing new land for that long,
such as 72h or 30d. asz collect and asz server remove at the end of a
pass, after the send, a conversation's files together, once its head
round covers them and, with metrics derived, every file of it is
derived. A conversation never sent is kept, and so is one holding
provider bodies, for now.

Each removed file is replaced by a skeleton, <name>.sk: Session Data,
compressed with gzip, that keeps every record and part in its place
with every identifier the index reads, a changes record's files and
lines without their diff, an execution record's join, time and outcome,
and no content. Its header records the file's digest,
size and record count, when and by which removal it was removed, and
what the receiver confirmed of it. The index rebuilt from skeletons is
the index rebuilt from the landed files, byte for byte, so a
conversation that becomes active again continues the same chain. Every
scenario run twice, once removing everything after each send, sent the
same files, rounds and metrics, byte for byte. On one root, 847.5 MB of
landed files had skeletons of 29.6 MB.

A removal writes removing.json first, then each skeleton, synced with
the error of its first sync checked, then deletes each file whose
skeleton reads whole and records its digest, then syncs every directory
of the removed range before the intent goes. The next pass finishes a
removal a crash or a power loss stopped, before anything lands, and no
collector lands into a session while its removal is unfinished.

Every reader of landed files reads a skeleton. The export never sends
one, verify reports removed files apart, the page marks removed steps,
asz show says a record was removed, LangSmith's next position goes on
from one, and asz repack refuses a session with skeletons. Metrics
turned on after a removal count the tokens a skeleton keeps.
@wu-sheng
wu-sheng force-pushed the retention-skeletons branch from c2775de to fd44be6 Compare October 13, 2026 02:40
@wu-sheng
wu-sheng merged commit 89dda17 into main Oct 13, 2026
24 checks passed
@wu-sheng
wu-sheng deleted the retention-skeletons branch October 13, 2026 02:53
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