Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions compaction-worker/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Will Porcellini

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
51 changes: 51 additions & 0 deletions compaction-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Pi Compaction Worker

Opt-in Pi extension for model-aware custom compaction.

It provides an extension-first slice for issue #595:

- resolves per-model/profile compaction budgets;
- starts a side summary job at a configurable prepare threshold;
- triggers compaction at a later threshold;
- validates prepared summaries against the current session/branch before use;
- falls back to live custom compaction, then Pi default compaction, on any failure.

The extension is disabled by default.

## Configuration

Add settings under `compaction-worker` in `.pi/settings.json` or `~/.pi/agent/settings.json`:

```jsonc
{
"compaction-worker": {
"enabled": true,
"prepareAtPercent": 60,
"triggerAtPercent": 75,
"reserveTokens": 16384,
"keepRecentTokens": 20000,
"summaryModels": ["google/gemini-2.5-flash"],
"profiles": {
"opus": {
"match": "anthropic/claude-opus-*",
"prepareAtPercent": 55,
"triggerAtPercent": 70,
"keepRecentTokens": 100000,
},
},
},
}
```

Use `/compaction-worker-status` to inspect the active model, matched profile, thresholds, and prepared-summary state.

Profile matching is deterministic: exact model matches win over globs, more specific globs win over broader globs, and declaration order is only used as the final tie-breaker.

Additional guardrail knobs are available when you need to coordinate with Pi's built-in compaction threshold:

- `builtinReserveTokens` (default `16384`) mirrors the host built-in reserve used to detect the built-in threshold.
- `builtinSkipMarginPercent` (default `0`) can reserve a small percentage band before that built-in threshold where this worker stands down and lets Pi's built-in compaction path win. Keep this at `0` unless you explicitly want that handoff band.

## Boundaries

This is an extension-side proactive policy. Pi core still owns built-in auto-compaction and overflow recovery thresholds. If a prepared summary is stale, divergent, expired, or incompatible with the current model/profile, the extension generates a live summary or falls back to Pi default compaction.
60 changes: 60 additions & 0 deletions compaction-worker/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as fs from "node:fs";
import * as os from "node:os";
import { join } from "node:path";
import {
CONFIG_ENV,
SETTINGS_KEY,
resolveBaseConfig,
type RawCompactionWorkerConfig,
type ResolvedBaseConfig,
} from "./helpers.js";

export interface LoadConfigOptions {
cwd?: string;
agentDir?: string;
env?: NodeJS.ProcessEnv;
}

function parseJsonFile(filePath: string): unknown | null {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[${SETTINGS_KEY}] Failed to parse config ${filePath}: ${message}`);
return null;
}
}

function readSettingsConfig(
settingsPath: string,
): { path: string; raw: RawCompactionWorkerConfig } | null {
if (!fs.existsSync(settingsPath)) return null;
const parsed = parseJsonFile(settingsPath);
if (!parsed || typeof parsed !== "object") return null;
const raw = (parsed as Record<string, unknown>)[SETTINGS_KEY];
if (!raw || typeof raw !== "object") return null;
return {
path: `${settingsPath}#${SETTINGS_KEY}`,
raw: raw as RawCompactionWorkerConfig,
};
}

export function loadConfig(options: LoadConfigOptions = {}): ResolvedBaseConfig {
const cwd = options.cwd ?? process.cwd();
const agentDir = options.agentDir ?? join(os.homedir(), ".pi", "agent");
const env = options.env ?? process.env;

const explicitSettingsPath = env[CONFIG_ENV];
if (explicitSettingsPath) {
const explicit = readSettingsConfig(explicitSettingsPath);
if (explicit) return resolveBaseConfig(explicit.raw, explicit.path);
}

const projectSettings = readSettingsConfig(join(cwd, ".pi", "settings.json"));
if (projectSettings) return resolveBaseConfig(projectSettings.raw, projectSettings.path);

const globalSettings = readSettingsConfig(join(agentDir, "settings.json"));
if (globalSettings) return resolveBaseConfig(globalSettings.raw, globalSettings.path);

return resolveBaseConfig(null, null);
}
3 changes: 3 additions & 0 deletions compaction-worker/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import rootConfig from "../eslint.config.mjs";

export default [...rootConfig];
Loading
Loading