mcpField
S
smart-compaction

smart-compaction

github.com/openduoAgent workflows1089 installsUpdated 4d ago
Repository

About smart-compaction

A smart-compaction skill for configuring idle-session context compaction in duoduo. It guides agents to tune thresholds using measured session statistics, backend caching behavior, and safety rules.

Key features

  1. 01Idle-time and context-size triggers
  2. 02Per-session and per-kind settings
  3. 03Compaction statistics and calibration guidance
  4. 04Break-even analysis using measured values
  5. 05Guardrails against unproductive compaction

Use cases

  1. 01Reducing context costs after idle periods
  2. 02Tuning thresholds for a session or runtime kind
  3. 03Disabling compaction for a conversation that must retain context

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/openduo/duoduo --skill smart-compaction

After installing, type/smart-compactionin Claude Code to confirm it loaded.

Details

From the project README

Smart Compaction — idle auto-compact management

What it is (one paragraph)

Channel sessions accumulate context; after an idle gap longer than the prompt-cache TTL, the next message re-writes the whole context into cache at a premium. The daemon ships a deterministic sweeper: when a LIVE session has been idle auto_compact_idle_minutes and its context exceeds auto_compact_min_context_tokens, it silently runs /compact. It is off by default everywhere; you (the agent) are the policy layer that enables and tunes it per conversation. Claude runs that /compact in-band on the live streaming subprocess. Grok uses the same knobs but executes _x.ai/compact_conversation (silent, no channel ack). Pi uses the same two knobs too, and pi compaction is two-layer: pi's own native threshold compaction stays on (inherited from the user's pi settings) alongside duoduo's idle auto-compact — both layers coexist by design. Codex remains the only excluded runtime. Grok compact returns no token counts, so the fuse and the compact notice never self-calibrate on a grok-only session — do not treat the first grok idle-compact as a calibration measurement. Pi is in the same boat: its compact result reports the pre-compaction size only (right after a compaction pi cannot measure the new resident context yet), so the fuse never self-calibrates on a pi-only session either. Pi is cheaper in the worst case, though — a session with nothing worth compacting answers with a free no-op instead of a paid no-gain compact.

The knobs (per instance, per kind)

duoduo session config <session|alias> get            # merged view + per-key source + measured stats
duoduo session config <session|alias> set auto_compact_idle_minutes=50 auto_compact_min_context_tokens=100000
duoduo session config <session|alias> unset auto_compact_idle_minutes   # back to kind default
duoduo session config --kind <kind> set ...          # kind-wide default (ONLY when the user explicitly says "kind"/"所有会话")
  • Recommended starting values (signed off for subscription/1h-TTL backends): idle_minutes=50 (fires inside the 1h cache TTL → the compaction read is 10× cheaper), min_context_tokens=100000 (≈2× the measured post-compact floor).
  • OFF = set auto_compact_idle_minutes=0. This RETAINS the calibrated threshold — the on/off switch and the calibration are separate fields (thermostat rule: switching off doesn't forget the set-point). A fresh start is an explicit unset.
  • Scope discipline: default to the CURRENT instance. Touch --kind only when the user explicitly widens scope.
  • set/unset take effect on the session's NEXT drain or the next 60s sweep — no restart. get never mutates anything.

Reading the stats — never mental-arithmetic what the kernel precomputed

session config get prints the session's compact_stats after each compaction (any origin — manual, auto, or the model's own reactive compact):

fieldmeaning
post_totalmeasured S_total: the floor compaction cannot go below (prompt chain + summary)
history_postthe compacted history size — this is the summary's OUTPUT cost (formula's O)
p_estimatefixed prompt-chain overhead (compaction can never reclaim this)
suggested_min_context_tokenskernel-computed 2 × post_total — your default retune target
g_gt1h_since_prev_compactcold (>1h-gap) returns between the last two compactions — the measured G
g_5m1h_since_prev_compactmid-gap returns — becomes cold too on 5-minute-TTL backends
idle_mshow long the session had been idle when the auto fire happened (absent for manual)
threshold_at_firethe min_context_tokens in effect when the auto fire triggered (absent for manual/reactive)

The compact notice (kernel → you)

On a smart-compaction-enabled session (and only there — sessions with the feature off get no injection), the first real turn after ANY compaction carries a <smart-compact-notice> block:

  • data line — compacted <ts>: ctx <pre>→<post>, history <pre>→<post>. post is the measured floor; no need to query stats for the basics.
  • mismatch line (only when mis-calibrated) — threshold <T> < suggested <S> — re-calibrate via smart-compaction skill. This is your retune signal: apply guardrail 4 immediately. It also means the kernel FUSE is now armed: auto-compact refuses to fire again while the threshold sits at or below the measured floor (a fire there can never get under the threshold — a pure no-gain loop), so retuning is what re-arms auto-compaction.
  • transcript line — the full pre-compact transcript path on disk. Compaction summarizes your context but deletes nothing: for lost RECENT detail, spawn a cheap subagent (haiku/sonnet) to search that file — do not read it whole into your freshly-slimmed context. memory/dossiers lag hours behind live chat; they are the long-term layer, not the recent-recall layer.

The break-even algorithm (only for the judgment the arithmetic can't make)

C* = (5·O + 1.25·G·S) / (1.25·G − w)
  • S ← compact_stats.post_total (measured, this session — never assume)
  • O ← the summarization OUTPUT size, i.e. the tokens the compact summary itself costs: compact_stats.history_post (the compacted history — ~4K typical). Do NOT use the history SHRINK (history_pre − history_post, which can be ~50K); that is what was removed, not what the summary costs, and using it inflates C* by an order of magnitude.
  • G ← g_gt1h_since_prev_compact (add g_5m1h too when the backend TTL is 5-minute-class). These are kernel-measured from the ledger — read them.
  • w ← trigger warmth: 0.1 if the backend cache TTL exceeds idle_minutes (subscription = 1h TTL), else 1.0 (metered API default, Bedrock, Vertex)

Domain: the formula is only valid when 1.25·G > w. When 1.25·G ≤ w the denominator is zero or negative — there are too few cold returns per cycle for compaction to pay for itself at any context size; that region is governed by guardrail 2 (raise the threshold or disable), not by C*.

Sessions with context above C* profit from compaction; below it, compaction loses money. TTL is a fact about the backend you must know or ask — it is deliberately NOT in the kernel.

G is a feedback variable, not a fixed input. The threshold you set determines how often the session compacts, which sets the length of each compaction cycle, which changes how many cold returns fall inside it — i.e. the very G you will measure next time. Treat each retune as one step of a loop: set → observe the next compact_stats (new G) → re-evaluate. Don't over-fit to a single cycle's G; watch the trend across a few compactions.

Guardrails (hard rules)

  1. Never set the threshold below 1.2 × measured post_total — below the physical floor, compaction is meaningless churn plus fidelity loss.
  2. Metered / short-TTL backends: keep 100000 only if measured G ≥ 2. For G≈1 sessions (one heavy task, one overnight return, re-fattened next day) the cold break-even is ≈305K — raise the threshold there or disable.
  3. Retune = act-then-inform: apply the change, then tell the owner in ONE line what changed and why (e.g. "已把这个会话的压缩阈值从 100K 调到 76K — 实测压缩地板是 38K"). Never silently, never with a wall of text.
  4. First auto-compaction IS the calibration (Claude, which writes compact_stats): when the first compact notice arrives, read suggested_min_context_tokens and retune the instance to it. No pre-calibration ritual is needed for new kinds. Grok does not emit that notice; skip this step on grok. Pi does not emit it either (its compact result has no post-compaction total) — skip on pi too, and tune pi thresholds by the break-even formula alone.
  5. When the owner says a conversation is precious ("这个会话的上下文别动"), set auto_compact_idle_minutes=0 on that instance and confirm.
  6. After any compaction, if you are unsure of an earlier detail: for recent specifics, have a cheap subagent search the transcript path the notice gives you; for long-term knowledge, consult memory/dossiers. Never reconstruct from the summary alone.
  7. Configure a kind AFTER installing its channel. --kind <kind> on a kind whose channel is not yet installed CREATES the kind config file; a later channel install then SKIPS its seed (the file already exists), so the channel comes up without its shipped defaults. Prefer instance-scope (config <session>) for anything but a deliberate kind-wide rollout.

Cache-less compatible backends

If the session runs on an endpoint with no prompt caching at all, every turn pays full input price on the whole context — compaction helps MORE there, not less. Same knobs, same formula with w=1 and cache premiums read as plain input cost.

Comments

Markdown supported