code-yeongyu / code-yeongyu/senpi
Feature request: configurable compaction model (`compaction.model`) — summarization always runs on the active session model
- Dominant language
- TypeScript
- Stars
- 429
- Forks
- 98
- Avg merge
- 5h 3m
- Merged PRs (30d)
- 526
Description
## Summary
`CompactionSettings` has no way to select the model that performs summarization. Compaction always runs on the **active session model**, so a session on an expensive model (e.g. `claude-sdk-oauth/claude-opus-5`) also pays Opus rates — and Opus OAuth quota — for every summarization, including speculative and idle compactions the user never asked for.
opencode already exposes this as `agents..compaction.model`. Requesting the equivalent in senpi.
## Evidence: no such knob exists today
`dist/core/settings-manager.d.ts` — every key of `CompactionSettings`, none of them a model:
```ts
export interface CompactionSettings {
enabled?: boolean;
reserveTokens?: number;
keepRecentTokens?: number;
speculativeEnabled?: boolean;
speculativeFraction?: number;
speculativeCooldownMs?: number;
restorationEnabled?: boolean;
restorationMaxItems?: number;
restorationMaxTokensPerItem?: number;
restorationMaxTotalTokens?: number;
restorationContextRatio?: number;
idleCompactionEnabled?: boolean;
}
```
`dist/core/extensions/builtin/compaction/speculative.js:247` — the snapshot hard-binds the active model:
```js
export function createSpeculativeCompactionSnapshot(context, options) {
const model = context.model;
if (!model) return undefined;
...
return { generation, expectedRevision, model, contextWindow, preparation, ... };
}
```
`dist/core/extensions/builtin/compaction/speculative.js:284` — summarization authenticates and generates against that same model:
```js
export async function runExtensionCompaction(context, snapshot, signal, onProgress) {
const auth = await context.modelRegistry?.getApiKeyAndHeaders(snapshot.model);
...
response = await generateSummaryMessage({ context, messages, prompt, snapshot, auth, ... });
}
```
There is no seam between "active model" and "summarizer model" anywhere on this path. A repo-wide grep for `compactionModel` / `summarizationModel` / `compaction.model` returns nothing in `@code-yeongyu/senpi` or `@earendil-works/pi-agent-core`.
## Why this matters (concrete case)
Main session on `claude-sdk-oauth/claude-opus-5`:
- With the default `resumeMode: "auto"`, `lane-policy.js` makes senpi stand down entirely (`SDK_NATIVE_LANE_REJECTION_REASON`), so the session can sit above the compaction threshold with `Compaction rejected: the Claude Agent SDK owns compaction for this session` and `Context remains above the compaction threshold because compaction did not complete`.
- The documented escape hatch is `claudeSdkOauthProvider.resumeMode: "off"`, which restores senpi compaction but flattens and re-sends the full history each turn (worse caching, more tokens).
- Once senpi compaction is back on, every summarization — proactive, speculative, idle — is billed to Opus, on the exact credential that is already the scarce resource.
A `compaction.model` would make that trade-off survivable: keep Opus for reasoning, run summarization on a cheap model.
Note that `omo.jsonc` users can already write `["opencode"].agents.build.compaction.model` and it is silently a no-op for senpi itself, which is an easy trap.
## Proposed behavior
Add an optional model override to `CompactionSettings`:
```json
"compaction": {
"enabled": true,
"model": "zai/glm-5.2"
}
```
Semantics:
- Unset (default) keeps today's behavior exactly: summarize on the active model.
- When set, `createSpeculativeCompactionSnapshot` resolves the override through the model registry and puts it in `snapshot.model`; `runExtensionCompaction` then authenticates and generates against it with no other change.
- `snapshot.contextWindow` should stay derived from the **session** model's usage (it describes the history being summarized), while the summarizer's own limits bound `boundSummarizationInput`. Worth deciding explicitly if the override has a smaller window than the session model.
- If the override cannot be resolved or has no credentials, fall back to the active model rather than failing compaction — a misconfigured summarizer must never wedge a session above the threshold.
- Optionally accept the same override per-agent/per-category, mirroring opencode's `agents..compaction.model`.
## Version
- `@code-yeongyu/senpi` 2026.8.11-4 (via `omo-ai` 5.0.0-0.beta.6)
- macOS arm64
- Provider/model: `claude-sdk-oauth` / `claude-opus-5`
## Checked for duplicates
Searched open + closed: `compaction model`, `summarization model`, `cheaper model`, `compact config`, `compaction settings`, `opencode compaction`, plus a full pass over all 28 open issues. Related but distinct: #765 (OpenAI remote compaction 15s timeout), #650 (unbounded overflow-retry loop), #527/#531 (caps and circuit breaker), #543 (runtime-registered provider auth), #296/#415 (codex-responses specific), #723 (retry re-bills full conversation). None of them request a configurable compaction model.
Contributor guide
Research direction
Start with dist/core/settings-manager.d.ts and the snapshot and execution paths in dist/core/extensions/builtin/compaction/speculative.js. Trace model resolution, authentication, and context-window handling, then verify that an unset or unavailable override preserves the active-model behavior while a configured override is used for summarization without wedging compaction.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- ai
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100