feat(storage): reclaim the prepared-request captures already on disk - #4738
Open
Astro-Han wants to merge 4 commits into
Open
feat(storage): reclaim the prepared-request captures already on disk#4738Astro-Han wants to merge 4 commits into
Astro-Han wants to merge 4 commits into
Conversation
6 tasks
Astro-Han
force-pushed
the
feat/reclaim-retired-provider-request-captures
branch
3 times, most recently
from
September 4, 2026 09:40
d8b0213 to
2e08ddc
Compare
Astro-Han
marked this pull request as ready for review
September 4, 2026 09:40
Astro-Han
requested review from
ARE404,
M4n5ter and
likun666661
and removed request for
ARE404 and
M4n5ter
September 4, 2026 10:12
Astro-Han
added a commit
that referenced
this pull request
Sep 4, 2026
Every model call stored a copy of the conversation. The prepared provider request was serialized whole into a private Artifact, and the record beside it carried up to 256 per-segment rows -- so one call cost tens of KB of database plus a file that grew with the Session it belonged to. Nothing read either: the capture's reader shipped in #1277 and was deleted in #2605, which kept the producer, and the per-segment detail's only consumer folded it into four byte totals. Both producers are gone. The fold now runs where the request is dispatched, and its result -- four byte totals plus a capped tool list, 1,971 B flat -- lands on the canonical ModelCallAttempt, which already owns the request's facts. Per model call with 60 tools and a 200-message conversation: database rows 46,077 to 1,971 B, capture file 136,967 B to none. The Artifact store also sealed one snapshot per Session on every load and every mutation, and a snapshot's revision hashes every record in its Session -- so 400 Sessions were sorted and hashed to answer a question about one. Every reader reloads the whole store from the database first, so a kept snapshot never survived to be read. Sealing one when a reader asks for it deletes the map, the two methods that maintained it, and the per-mutation bookkeeping: one listPage at 6,000 records goes 13.45 to 11.54 ms. Compatibility: every decoder stays. `hasExactShape` fails a whole record on an unknown key, so removing `captureArtifactId`, the `provider_request_capture` source, or `PreparedRequestObservation` and its validator would strand exactly the records this stops producing more of -- including their usage and cost. Captures already written are left on disk; #4738 reclaims them. One model-visible change: a sub-agent's spawn tool result listed the private capture in `artifactIds` / `artifactCount`. A child turn now stores nothing of its own, so that list is empty. Also gives `graceful Host shutdown stops and drains an active Turn` the checkpoint its sibling test already used, so the state its drain finds is not left to how fast the machine is. Closes #4082 Refs #4037 Refs #4704 Generated-by: Claude Code
Removing the capture sink stops the growth but leaves the residue, and the residue is not the user's to clear: captures are `userVisible: false`, so no UI lists them, and the only thing that ever deleted one was purging its whole conversation. One workspace measured here holds 772 MB of them. The store made them, so the store disposes of them. `purgeRetiredCaptures` takes a bounded batch through the same mutation queue and purge-intent file as every other deletion, and reports what is left; the sweep started at host composition drains the rest behind live turns and stops when there is none. A store that never held captures does one empty pass. Interrupting it is safe by construction rather than by a checkpoint: each batch is durable on its own and the next pass reads whatever remains, so a crash, a close, or a stop all resume the same way. `purge` now shares its body with the sweep instead of restating it. Refs apache#4037 Generated-by: Claude Code (cherry picked from commit 89628f2)
`captureArtifactId` was a required mapping, so once the sweep reclaimed a capture Artifact, branching or copying any Session holding a historical model call threw and could never succeed again. Every other Artifact reference may keep throwing: the bytes and the events naming them have always been removed together. This one cannot, because the sweep removes bytes an append-only ledger still names — so the key now leaves with them and the attempt keeps its record without the join. Also drops the two capture decoders left behind by apache#4631, which retired both provider-request event types from the emitted catalogue: a copy no longer reaches either one. Refs apache#4037 Generated-by: Claude Code
A write authority refuses every mutation until it has recovered, and the sweep gives up after one failure. Starting it at composition meant its first batch always landed before recovery ran, so it reclaimed nothing and never retried. Both new tests recovered first and missed it. Refs apache#4037 Generated-by: Claude Code (cherry picked from commit d25a8ed)
A batch holds the writer lock and its cost rises with everything the store holds, so a fixed 250 ms pause would not stay behind live turns on a store big enough for the sweep to matter. The pause is now at least three times the batch it follows. Also drops a sweep assertion that could not fail: the batch size is a module constant, so asserting it is a positive integer pinned nothing. Generated-by: Claude Code
Astro-Han
force-pushed
the
feat/reclaim-retired-provider-request-captures
branch
from
September 4, 2026 10:30
2e08ddc to
70fe70b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
#4722 stops producing prepared-request captures. This reclaims the ones already on disk. On my own 814 MB installation they are 772.7 MB — 87% of the Artifact population, and 99.4% of their bytes are re-serialized duplicates of messages already in the ledger.
A bounded background sweep drains them through the same purge-intent path a Session delete uses, then stops. It reclaims the files and the Artifact rows naming them — nothing else. The per-segment detail #4722 stopped writing sits in the AgentRun ledger, which is append-only by design, so those rows stay where they are and only new calls are cheaper.
#4722 has landed, so this now stands on its own: 4 commits on current
main.Refs #4037, #4704.
🔀 What this changes about deletion
flowchart TB subgraph BEFORE["until now"] direction LR P1["purgeSessionArtifacts"] --> B1["Artifact bytes"] P1 --> E1["events naming them"] end subgraph AFTER["this sweep"] direction LR P2["purgeRetiredCaptures"] --> B2["capture bytes"] E2["attempts naming them<br/>append-only, cannot be retracted"] end BEFORE -.-> R["⇒ a capture reference<br/>must tolerate a missing referent"] AFTER --> R classDef gone fill:#fcebeb,stroke:#e24b4a,color:#a32d2d classDef live fill:#e1f5ee,stroke:#1d9e75,color:#0f6e56 classDef rule fill:#fff6e0,stroke:#d99a1f,color:#8a5f00 class B1,E1,B2 gone class E2 live class R ruleArtifact bytes and the events naming them have always been removed together. This sweep is the first mechanism that removes bytes while the events referencing them live on, because the AgentRun ledger is append-only: a historical
ModelCallAttemptkeeps itscaptureArtifactIdforever.That is the review question — who reads a capture reference, and does it survive the referent being gone.
captureArtifactIdin a conversation copy was a required mapping. Once a capture was reclaimed, branching or copying any Session holding a historical model call threwConversation copy is missing Artifact …and could never succeed again. The key now leaves with the bytes; the attempt still copies and still decodes.conversation-copy.ts— attachment refs, tool-result Artifacts, snapshot lists, linked children — still throws on a missing target, and should. The sweep filters onsource === 'provider_request_capture', and none of those name that source.captureArtifactIdstays in theModelCallAttemptschema and its validator:hasExactShapefails a whole record on an unknown key, so dropping the field would strand exactly the records this exists to free the bytes of.Also removes the two capture decoders #4631 left behind in
conversation-copy.tswhen it retired both provider-request event types from the emitted catalogue — a copy no longer reaches either.🧹 The sweep
openedArtifactStore.recover()— a write authority refuses every mutation until it has recoveredmax(250 ms, 3 × last batch)— a batch holds the writer lock and its cost rises with the store, so a fixed pause would not stay behind live turnsEach batch is durable on its own, so stopping only means the next one does not start; a later run continues from the residue.
✅ Verification
format,lintclean.test:dist:@maka/storage1,122,@maka/runtime3,217,@maka/runtime-host1,687,@maka/core807 — 0 failures.typecheckalso on@maka/desktop,@maka/ui,@maka/mcp,@maka/eval,maka-agent.rewriteOwnedArtifactIdback:Error: Conversation copy is missing Artifact artifact-gone.AI use
Select exactly one:
Tool(s) and scope: Claude Code — traced the reference points, wrote the implementation and tests. Reviewed and verified by me.
Checklist
Does this PR entail a change in behavior?