feat(providers): manual context compaction for every provider - #430
Open
rynfar wants to merge 6 commits into
Open
feat(providers): manual context compaction for every provider#430rynfar wants to merge 6 commits into
rynfar wants to merge 6 commits into
Conversation
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com> (cherry picked from commit c5ba51d629b3813182cf3e161cc3f23b1e541dc3)
…623) Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com> (cherry picked from commit 5f878d2a85807618a4c8571cdef5daa3124672d6)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 5fa35d211682ee02e34fba0711838ca431ed003b)
Compaction never becomes a provider turn, so it must not keep the turn admission Pylon's decider reserves for every thread.turn.start; the busy check becomes an ownership test because Pylon admits a turn before the reactor sees its intent; and the compaction label reads a retained pair of context-window totals because Pylon upserts one row per thread instead of appending one per token tick. Also records provider.session.stop.failed, which existed in the activity kind union but was never appended, and stamps the test fakes' events with their session incarnation the way real adapters do.
Contributor
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
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.
Manual context compaction worked for exactly one provider in Pylon: Prime, through
its own richer control surface. Every other provider had no way to compact a thread.
This adopts upstream's compaction command, so
/compactnow works on Codex,Claude, Cursor, Grok, OpenCode and Antigravity — natively where the adapter
supports it, and as the provider's own slash command everywhere else.
What changed
Adapters declare how they compact:
native— the adapter's own compaction callslash-command/compactslash-command/compresssession.compaction.updatedcontrol surfaceProviderService.compactThreadroutes to the declared strategy, watches theruntime stream for the compacting turn, and settles when the provider reports a
compacted thread state or the turn ends. The
/compactmessage never reaches theprovider as ordinary text; it becomes a compaction request in the reactor.
Compaction rows render as their own timeline entry on web and mobile, labelled
with token counts when they can be derived, and fold under a settled turn fold
alongside the rest of that turn's work.
Adopted from T3 Code
c5ba51d629b3813182cf3e161cc3f23b1e541dc3(#9293) context compaction command5f878d2a85807618a4c8571cdef5daa3124672d6(#9623) fold compaction under settled turn folds5fa35d211682ee02e34fba0711838ca431ed003b(#10112) adapters declare context compactionPorted at the frozen upstream head as one change, so the adapter surface lands in
its final
compactionshape rather than passing through #9293's intermediatecompactThreadfield.Pylon divergences reconciled
Each of these is a place where a mechanical port compiled cleanly and behaved
wrongly at runtime.
processRuntimeEventdrops any eventwhose
sessionIncarnationIddoes not match the thread's current incarnation —that is what stops a replaced adapter writing into a new session. Upstream has
no such gate, so its test fakes emit unstamped events. Left as-is, every
compaction-terminal event was discarded and each compaction hung to its full
timeout. The fakes now stamp their events the way real adapters do; the fence is
untouched.
admission for every
thread.turn.start, including/compact, but compactionnever becomes a provider turn that could accept it.
restoreCompactionnowretires those fields when it returns the session to ready — without that, one
/compactleft the admission set forever and the decider rejected every laterturn on the thread.
reactor observes its intent event, so the thread already reads
startingforthe compaction's own request. Upstream's
status === "starting"guard wouldreject every compaction; it is now an ownership test against
activeTurnIdanda differing
pendingTurnRequestId.contextWindowActivityIdis a stable per-thread key, so Pylon keeps one
context-window.updatedrowinstead of appending one per token tick. Upstream derives
899K → 19Kbyscanning two successive rows, which cannot work here. A bounded
contextWindowHistoryByThreadIdcache retains the last two totals instead, sothe label survives without reintroducing a row per update.
immediately after publishing; Pylon's
processRuntimeEventstill has admissioncleanup and session teardown to do, so the interception is a publish decorator
rather than an early exit.
Fixed along the way
provider.session.stop.failedexisted in the activity kind union but nothing everappended it, so a provider stop that failed was silent. Every stop path in the
reactor now runs through
withSessionStopTracking, which records the failure withits cause.
Verification
knip:checkclean.Three upstream test expectations were adapted to Pylon's session model rather than
to weaken them, each commented in place: concurrent turns during compaction are
refused by Pylon's decider instead of recorded as a reactor activity, and Pylon's
decider persists the
stoppedtransition in the same transaction as the stopintent, so a session reads
stoppedwhere upstream still readsstarting.No migration. No new dependency.
Model and harness: Claude Opus 5 via Claude Code.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.