Conversation
…d tool Staged rollout of the jev-compaction port (design source: Waxmell114514/jev-compaction, MIT). New internal/compaction package: paragraph segmentation, System One scoring backends (resolve via env, key-file lookup, per-backend URLs), a pipeline that scores tool outputs and appends decisions to a shadow log (~/.local/share/late/compaction-shadow.jsonl), and relocation that elides low-scoring segments into [[elided ...]] pointers backed by an in-memory store. Executor integration: SetToolResultCompactor installs the pipeline process-wide (root agent + subagents); results over MinCompactToolResultChars (4000) are offered to the stage. Shadow mode (default) scores and logs without changing any result; enabled mode additionally relocates and registers the expand tool so originals stay retrievable. Everything fails open: backend errors, canceled contexts, and shadow-log failures keep the original tool output. Scoring calls go through a package-local scoring slot (internal/compaction/llm_slot.go). Modes resolve as --compaction-mode > config compaction-mode > shadow; invalid values warn and fall back to shadow. --compaction-threshold (default 0.35) sets the elision score cutoff in enabled mode. Docs: quickstart section for context compaction; README feature bullet.
…ev scoring, elision) Session.CompactContext walks the whole history (not just tool outputs) and relocates low-scoring segments into the elide store: - Frozen prefix: the first max(1, len/4) messages (25%) stay byte-identical, so the prompt-cache anchor - the system prompt at index 0 plus the earliest exchanges - never moves across compactions. - User messages are immune; assistant and tool-result contents are compacted in place with roles, ToolCalls, and timestamps preserved (ToolCalls are structurally required and never touched: only Content shrinks). - Segments are scored against the ongoing task (the last user message); anything scoring strictly below the threshold is replaced by one [[elided ...]] pointer line whose original lives in the store for the expand tool. A nil store disarms relocation: a pointer whose original cannot be stored must never enter history. - Shadow mode (ShadowOnly) runs the full scoring walk and computes the honest would-save report without mutating history or storing originals. - Fail-open mid-walk: a scorer error stops the walk, already-rewritten messages stay valid (their pointers and stored originals resolve), and the error reports how far the walk got. Persistence is the callers job (SaveHistory), so compaction never breaks a session. - Token math: CompactionReport carries EstimateMessageTokens before/after, tokens saved, and the byte size of what was relocated into the store.
Manual command: /jev-compact-context runs one full-history compaction pass on the session behind the TUI. It requires compaction-mode other than off (nil Compactor reports "compaction unavailable"); under shadow mode it runs report-only. The run executes off the TUI update loop (network scoring can take seconds) and reports via compactionResultMsg. Auto-trigger: with jev-autocompact on (config.ResolveAutocompact; default false) and the focused agents context usage crossing jev-autocompact-percent (default 99, valid 1-100) of the context window, the same compaction runs automatically. It fires once per crossing - the agent stays disarmed until usage falls below (percent-9)% (typically right after a compaction shrank the history) or /new starts a fresh conversation. Shared CompactionRunning guard: exactly one CompactContext run (manual or auto) may be in flight at a time; both paths set it on dispatch and the result message clears it. Store id-space unification: the elide-id counter moves into compaction.Store (NextID/Put exported), so tool-output and history compaction mint pointers from one id space and "elide-<n>" always names exactly one original. Pipeline-to-TUI plumbing: Pipeline.HistoryScorer exposes the decision client as the session.HistoryScorer; cmd/late wires historyCompactionRunner (session.CompactContext with the pipelines threshold, persisting mutated history via SaveHistory - skipped for shadow runs, and with a fresh private store there since shadow mints pointer ids without an expand tool to read them). NewModel resolves the autocompact settings; late -h documents the feature under "Context compaction".
quickstart.md: extend the Context Compaction section with /jev-compact-context (manual full-history compaction - frozen prefix, Jev scoring of every post-prefix message, elision into pointers the expand tool resolves; requires compaction-mode other than off, report-only under shadow) and the jev-autocompact / jev-autocompact-percent config keys (auto-trigger at the configured context-usage percent, once per crossing, re-arms after compaction). README.md: one feature bullet next to the tool-output compaction entry.
ScoreBatch fail-opens with a complete score map; CompactContext now continues the walk with those scores and surfaces the scorer errors at the end. Only incomplete score maps abort (stopped after N messages). Adds CompactionReport.MessagesScored.
…ow log Compaction statuses now report scored/scanned message counts; every history-compaction run appends one type=history-run summary line (scanned/scored/elided/tokens/error) to the shadow log; replay skips summary lines.
Ports the reference's safety nets: per-kind elide floors (stacktrace/diff protected at 0.05), max-elide-fraction tripwire (distrust the scorer, keep everything), min-gate token floor (no backend call for small outputs), segment kinds (text/json/log/stacktrace/table/code/diff), and config knobs compaction-max-elide-percent / compaction-protected-floor.
…e reconstruct Pointers are now [[elided id=r:<8hex> lines=a-b tokens=N "summary"]] with sha256 content-addressed ids, line ranges, 120-char escaped summaries, and a Reconstruct that substitutes pointer+newline with the stored originals byte for byte. Consecutive elided segments merge into one record per run; legacy elide-N ids still parse and expand.
Records (text, kind, origin, tokens, summary, segment_ids, expand/hit counts) persist to ~/.local/share/late/compaction-store.jsonl as an append-only JSONL log, loaded on startup — [[elided]] pointers no longer dangle after a restart. Enabled mode shares one store across the pipeline, the expand tool, and history compaction.
… -replay-shadow Every expand records an outcome for the record and each contributing segment (the reference's false-negative attribution); decision entries record the threshold consulted; ShadowLog grows Stats/FalseNegativeRate/ ReplayTable (kept, relocated, tokens saved, still missed per threshold); new read-only CLI: late -replay-shadow=0.10,0.35,0.50.
A persisted, monotonic compaction high-water mark makes the frozen prefix append-only: never re-scores pointer-bearing messages, fails loudly with ErrFrozenPrefix on any below-mark mutation, advances only on a completing mutating walk (shadow never advances), and rewinds/rolls with /new, Rewind and PopLastUserMessage.
Auth (401/403, poisons the client — zero further requests), Validation (4xx, never retried), Budget (pre-send oversized item), Unavailable (retried, then fail-open). Pipeline warns once and disables scoring on auth; history compaction stops immediately on auth; oversized items keep per-item keepScore with a typed cause.
late -check-compaction runs the reference's three-stage verification against the real backend (questions parse, gate relocates, pointer expands byte-for-byte) and names the broken stage; startup probes the backend once after the TUI is up and disables scoring on typed auth.
Scores the store's digest against the current task (reference RETRIEVE_QUESTION) and appends the top-k relevant records as an ephemeral, unpersisted system block at the end of the work area. Default off; compaction-retrieval enables it; retrieve decisions are kind-tagged and excluded from the admit false-negative rate.
compaction-backend: "offline" runs the full gate→pointer→expand flow with a deterministic scripted scorer (no key, no network, reference testing.py parity); Pipeline scores through a Scorer interface; docs cover the whole shipped surface and credit the reference repo.
Shared BuildElidedRun for both relocation surfaces (was duplicated); stale package/tool docs corrected; deliberate -replay-shadow-before- config position documented; store no-fsync debt documented.
The Go client invented {type,prompt} questions and map-keyed state
items; the real endpoint rejected every scoring request with 400
invalid_union at questions.<id>.instructions. Ported the reference
shape verbatim: state.items is an array of {ref,text}; each question is
{noul, 'Considering item <ref> only: ...', criteria{true,false}} with
the task only in state.task; answers parse as {type:'noul',noul:x}
objects (bare numbers tolerated for local gateways).
Live proof: late -check-compaction → PASS 4/4 stages against the real
OpenRouter decisions endpoint.
Closed
5 tasks
Owner
|
Thank you for doing it this way! I will take a careful look at this and test it a bit. |
Contributor
Author
wait to merge, there are some bugs i'm fixing right now.. i will update this and other PRs with the fixes.. |
…tics Compaction's mid-session warnings (the pipeline's one-time auth-poison note and the retrieval-skip notice) must not paint raw fmt.Fprintf(os.Stderr, ...) text over the bubbletea alt-screen, where it garbles the footer and displaces the agent-name row. This adds the TUI surface they route through: a DiagnosticMsg that renders as a WARNING toast with a 6s expiry and the standard clear tick, truncating long text to the terminal width with the bar's ellipsis helper. Empty text is ignored. Adapted from the broader mid-session diagnostics commit: the plugin- manager, orchestrator, and tool-package sinks and their wiring stay with the TUI diagnostics PR; only the toast surface compaction needs lands here. stderr remains the fallback for headless/CLI flows.
…d config 413 is now a typed, never-retried error whose text tells the user exactly what to do; a one-shot compaction recovery fires on 413 when compaction is enabled (re-armed by /new). The elision score threshold is now settable in config.json (compaction-threshold, flag > config > 0.35 default), and the post-compaction status says the context bar is an estimate until the next request's usage arrives. Port note (feat/jev-compaction): main.go detects the explicitly passed -compaction-threshold flag with a local flag.Visit check — the explicitFlags map from the config-CLI-parity work belongs to the config PR and is not part of this branch.
…harper prompt, error log Real-output audit of the first production compaction found: oversized paragraphs were cut into pieces and elided piecemeal (corruption risk), 8 pure-prose assistant messages were compacted (one losing the assistant's closing question), and activate_skill results were elidable. Cut pieces now share one atomic elide decision (min sibling score), prose-only assistant messages are never candidates, activate_skill tool results get a 1.0 protection floor, and the admit question distinguishes progress noise from concrete non-re-derivable facts. Critical errors (walk aborts, save failures, auth poisoning) now append to a durable late-errors.log; all data paths route through one pathutil.LateDataDir. Port note (feat/jev-compaction): main.go's diag closure (with its LogError routing) lands with the warning-sink wiring commit, where it is first used — the plugin/orchestrator/tool sink wiring it served on the integration branch stays with the TUI diagnostics PR.
docs/config-reference.md documents every config.json key this build reads (provider/subagent settings, supervision, tools, and the whole context-compaction block: mode, the compaction-threshold score cutoff with its flag > config > 0.35 precedence, the info-bar percentage, gate knobs, offline backend, autocompact, retrieval), the file location, how values are parsed and validated (fatal type errors, the located did-you-mean error for unknown keys inside models[] entries, warn- and-fall-back for value-range problems), starter and fuller examples, and the nested schemas. A reflection test keeps the tables in lockstep with the Config struct in both directions. Port note (feat/jev-compaction): scoped to the keys this branch's Config struct actually has — the CLI-parity keys, boolean-synonym table, strict top-level parsing sections, and flag-precedence rules from the config PR are left for that PR's version of the doc, which extends this one.
… routing Stale toast ticks no longer clear newer toasts (expiry-aware clear: a tick scheduled by a PREVIOUS toast lands while a newer one is alive and must not kill it early; the toast-set path schedules its own real clear tick instead of clearing on the next loop iteration). Rewind re-arms the one-shot 413 payload-recovery compaction exactly like /new does. The compaction pipeline's one-time auth-poison warning and the retrieval- skip warning route through the TUI diagnostic sink (Pipeline .SetWarningSink + main's diag closure, which also appends every diagnostic to the durable late-errors.log) instead of raw stderr; headless flows keep the stderr fallback. The end-to-end compaction test now annotates the assistant message with a tool call (pure-prose assistant messages are never compacted). Port note (feat/jev-compaction): taken from the TUI review-fixes and strict-parser-hardening commits — only their compaction-relevant parts. The KeyReleaseMsg rework, todo-pane rune release, ANSI-aware truncation, and the orchestrator/plugin/tool diagnostic-sink propagation stay with their own PRs.
…ening The group-min reduce could elide a protected piece inside a cut paragraph (min-floor inversion): a sibling at the unelidable ceiling now pins the whole group to keep, with the shadow entry recording the same decision so -replay-shadow reproduces it. Error log: lazy-open no longer clobbers an explicit install; oversized messages truncate rune-safely with a marker; rotation absence documented. Group id scoping documented.
jev-autocompact-percent is now also valid inside each models[] entry; an agent's trigger resolves as its model entry's value (1-100) > the global > 99 — different context sizes need different triggers. Out-of-range per-model values warn at startup and fall back to the global. Strictness extends to models[] entries too: unknown keys inside an entry are located errors with did-you-mean suggestions (the typed decode would silently drop the typo), enforced by a models[] key walk so a mistyped per-model override key cannot silently no-op. The TUI's autocompact trigger resolves the watched (focused) agent's model entry through agent_models before falling back to the global. Port note (feat/jev-compaction): the models[] entry key walk is inlined here with minimal private helpers (known-key set, positioned rendering, Levenshtein did-you-mean) — the full positioned strict parser for every top-level key belongs to the config PR, as do the Degraded defense-in-depth tests this commit's test file touched on the integration branch.
…e.Until golangci-lint v2.13.2 findings on feat/jev-compaction: - relocate.go:439 (ineffassign): elidedTokens = 0 after the tripwire resets elide[] is never read again; drop the assignment. - relocate.go:489 (staticcheck SA4010): kept is appended but never read — kept text is written to the builder directly; drop the slice and the append. - client.go:754 (staticcheck S1024): date.Sub(time.Now()) -> time.Until(date). Reproduced locally with golangci-lint v2.13.2; full go test ./... -race -count=1 green.
History compaction now honors the same GateConfig protections as the tool-output path (per-kind floors, per-message max-elide-fraction tripwire with shadow entries), expand results are never re-compacted, the auto-compact trigger watches the session it actually compacts (root) instead of the focused agent, and the fast/slow token counters share one walk so first-paint estimates are structurally identical.
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.
Update 2 — deferred follow-ups implemented
All four follow-ups deferred at review time are now implemented, in
f8fd137(feat(compaction,tui): implement deferred follow-ups):compaction.GateFloorForinstead of a flat threshold; themax-elide-fractiontripwire runs per message (a message whose elided fraction would exceed the gate's fraction keeps everything); tripped messages are recorded in the shadow log when one is threaded, and the run summary reports them asCompactionReport.Tripwires. A nil gate keeps the legacy flat-threshold walk.protectedHistoryTool = ProtectedTool || expand, so a tool result that came back from anexpandcall is skipped by the history walk exactly like the tool-output path's protected kinds.event.ID == m.Root.ID()), not the focused agent's — the compaction pass always rewrites the root session's history, so a focused subagent's usage can neither fire nor suppress the trigger.calculateHistoryTokenswalk over the same message fields, so the streaming estimate is structurally identical to the count the compaction decisions use.docs/quickstart.mdgained one line stating it: history compaction (/jev-compact-context, the auto-trigger, and the 413 recovery pass) honors the same protections — the same keep threshold and protected-kind floors, with the max-elide-fraction tripwire applied per message (a tripped message keeps everything). Fullgo test ./... -race -count=1andgolangci-lint v2.13.2are green on the branch head.Update 2026-09-25
Seven new commits since the original description, at
2fdfbc1:aa8e6cb) — a 413 is now a typed, never-retried error whose text says what to do; a one-shot compaction recovery fires on 413 when compaction is enabled (re-armed by/new); the elision score threshold is settable in config.json ascompaction-threshold(flag > config, 0.35 default).70aafe2) — atomic cut-paragraph elision, prose-only assistant protection,activate_skill1.0 floor, and a sharper admit question that distinguishes progress noise from non-re-derivable facts.pathutil.LateDataDirunification (70aafe2,ace1ed3) — critical errors land in a durablelate-errors.log(lazy-open hardened inace1ed3), and all mutable data paths route through onepathutil.LateDataDir.397a6a4,8ac3c7c) — a newDiagnosticMsgrenders mid-session warnings as proper warning toasts instead of raw stderr over the alt-screen, stale toast ticks no longer clear newer toasts, and compaction's one-time notices route through the TUI pipeline warning sink.ace1ed3) — the keep decision is deterministic and reproducible by-replay-shadow.jev-autocompact-percentoverride (2fdfbc1) — withmodels[]key validation.131f4b0) —docs/config-reference.mdscoped to this branch's schema.Reference-faithful port of jev-scored context compaction, split out of #133 so the TUI work and compaction can be evaluated separately (per review). This body addresses the review comments head-on.
Separation from the TUI work
The only TUI surface in this PR is one slash command —
/jev-compact-context— plus its status text. Every other TUI change from #133 (status bar agent type, todos pane, info bar + timestamps, config error surfacing) lives in separate PRs and is independently evaluable.No mandatory, no automatic compaction
compaction-modedefaults toshadow: tool outputs are segmented and scored, decisions are appended to the shadow log — and nothing else. Zero behavior change; no tool result is ever rewritten or dropped in shadow mode.jev-autocompactdefaults tofalse. Automatic compaction only ever runs if the user turns it on (and tunesjev-autocompact-percent, default 99).enabledmode is strictly opt-in and still fail-open: a scorer outage (or any error) keeps the original output, nothing is ever deleted — elided text stays retrievable byte-for-byte through theexpandtool. User messages are never compacted.Investigation before hype — the tests the review asked for
Preflight, live against the real decisions endpoint.
late -check-compactionruns three real stages — questions (a minimal score batch parses, every id returns a numeric score), gate (a real ~2KB tool output actually relocates into pointers + store records), expand (the pointer reconstructs byte for byte) — and exits 0/1. Live run againsthttps://openrouter.ai/api/alpha/decisions, model~typesafe/jev-latest:Read-only verified: the preflight left
config.jsonand the state dirs byte/mtime-identical.Replay with false-negative rate.
late -replay-shadow 0.10,0.35,0.50replays the recorded shadow log (5,000+ real scored decisions) at any thresholds — kept/relocated/tokens-saved/still-missed plus the false-negative rate (a segment elided at its own recorded threshold was later expanded). Read-only:Offline scripted scorer for evaluation.
config.json"compaction-backend": "offline"swaps the scorer for a deterministic local scripted scorer — the whole flow and the preflight run with no API key and no network (the model URL is a dead port to prove it):Typed error taxonomy. Scorer failures surface as auth / validation / budget / unavailable — never retried blindly, no retry storms, and always fail-open.
Prompt-cache safety. History compaction works on an append-only frozen prefix with a persisted high-water mark: the anchor is never mutated across runs, and a walk that would violate the prefix fails loudly (
ErrFrozenPrefix) and changes nothing.Durable pointers. A persistent file-backed record store means
[[elided …]]pointers survive restarts and are shared with subagents.Gate tripwires.
max-elide-fraction(never elide more than that fraction of a run in one pass) and protected segment kinds — stacktraces and diffs carry a score floor, so a scorer's bad day can't erase the two things you usually need verbatim.Wire contract. This port follows github.com/Waxmell114514/jev-compaction exactly — including the fix that made live scoring work: an earlier draft sent a score-batch payload the real endpoint rejected (400, missing
questions.<id>.instructions); the preflight caught it before any integration use, and the request shape now matches the reference wire contract precisely.The scorer is swappable by design — backend registry today:
typesafe/openrouter/gateway(--api/JEV_API), plus the offline scripted scorer for evaluation.What's in the box
Segmentation · scoring · shadow log ·
[[elided …]]pointers +expandtool ·/jev-compact-context·jev-autocompact(opt-in) ·retrieve()read-side injection (config-gated, off by default) · docs.Testing
exit 0 — all 19 packages green (16 ok, 3 without test files).
gofmt -lon the touched files: no output (clean)go vet ./cmd/late ./internal/compaction ./internal/config ./internal/executor ./internal/orchestrator ./internal/session ./internal/tool ./internal/tui: cleanbc44df9); the offline preflight ran in a scratch home with a dead model URL.