| name | smart-compaction |
| description | Manage duoduo's idle auto-compact for channel sessions — enable/disable per conversation, read the measured compaction stats, and retune thresholds with the break-even formula when the owner changes model/backend or asks about idle-session cost. Trigger when the owner mentions 自动压缩/smart compaction/压缩策略, asks why a session was compacted (a compact notice appeared), switches a session to a metered or short-cache-TTL backend, or asks to protect a conversation from compaction. Also use proactively right after the FIRST auto-compaction of a session you manage — that event carries the calibration measurement. |
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 in-band. It is off by default everywhere; you
(the agent) are the policy layer that enables and tunes it per conversation.
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):
| field | meaning |
|---|
post_total | measured S_total: the floor compaction cannot go below (prompt chain + summary) |
history_post | the compacted history size — this is the summary's OUTPUT cost (formula's O) |
p_estimate | fixed prompt-chain overhead (compaction can never reclaim this) |
suggested_min_context_tokens | kernel-computed 2 × post_total — your default retune target |
g_gt1h_since_prev_compact | cold (>1h-gap) returns between the last two compactions — the measured G |
g_5m1h_since_prev_compact | mid-gap returns — becomes cold too on 5-minute-TTL backends |
idle_ms | how long the session had been idle when the auto fire happened (absent for manual) |
threshold_at_fire | the 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)
- Never set the threshold below 1.2 × measured
post_total — below the
physical floor, compaction is meaningless churn plus fidelity loss.
- 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.
- 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.
- First auto-compaction IS the calibration: 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.
- When the owner says a conversation is precious ("这个会话的上下文别动"),
set
auto_compact_idle_minutes=0 on that instance and confirm.
- 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.
- 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.