Skip to content
Merged
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
3 changes: 2 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ The agent maintains an optional **`manage_tasks`** list (create/update via the h

#### Context compaction (the compaction governor)

When a cycle's input tokens cross a threshold, the director compacts the inference-facing history (the full run is always retained in the context store). The threshold is **model-aware** — roughly 60% of the active model's real context window — so small-window models compact early enough to avoid provider context-overflow while large-window models do not compact prematurely. The compacted prefix is **append-only across passes**: the existing compacted user turn stays byte-identical; new folds become later summary turns with a harness-inserted assistant spacer (identified by reserved `model: "harness"`, plus a visible sentinel; persisted `[compaction]` tokens without a producer still freeze) between them so the prompt head can remain in the provider KV cache. Model-emitted copies of the spacer are not frozen. Spacer-only model replies are incomplete: ChatDirector nudges, then falls through loop-protection, workflow-idle, and open-task rails rather than empty-settling with work still open. The governor covers four cases:
When a cycle's input tokens cross a threshold, the director compacts the inference-facing history (the full run is always retained in the context store). The threshold is **model-aware** — roughly 60% of the active model's real context window — so small-window models compact early enough to avoid provider context-overflow while large-window models do not compact prematurely. The compacted prefix is **append-only across passes**: the existing compacted user turn stays byte-identical; new folds become later summary turns with a harness-inserted assistant spacer (identified by reserved `model: "harness"`, plus a visible sentinel; persisted `[compaction]` tokens without a producer still freeze) between them so the prompt head can remain in the provider KV cache. Model-emitted copies of the spacer are not frozen. Spacer-only model replies are incomplete: ChatDirector nudges, then falls through loop-protection, workflow-idle, and open-task rails rather than empty-settling with work still open. The governor covers five cases:

- **Threshold at a tool pause** — Once over threshold, the follow-up `infer` after a tool batch is swapped for a `compact` cycle, and inference resumes via a host continuation message. After a compact that remains over the high watermark, the governor uses **growth hysteresis** (wait for usage to grow by ~10% of the window) instead of re-arming on every cycle; dropping under 60% is not required.
- **Idle (end-of-turn)** — An interactive turn can end with a reply and then sit idle with no tool batch to intercept; the governor requests a continuation at that pause and compacts when it arrives. An operator message that races the continuation still compacts first, then re-enters inference to answer it.
Expand All @@ -204,6 +204,7 @@ When a cycle's input tokens cross a threshold, the director compacts the inferen

The `ollama` table key is the bare provider segment; the harness stamps slash-form on `sourceId` (`ollama/default` / `ollama/<instance>`), with `provider: openai-compatible` (`buildOpenAISource`) and a bare model (`llama3` / `qwen3`). Idle-recompress disable matches via `isOllamaProviderId` on that `sourceId` (`src/provider/ollama.ts`), not by looking up slash-form as a table key.

- **Operator `/compact`** — A first-class slash command folds now without waiting for the occupancy governor. Optional trailing instructions go to the summarizer (empty uses the default structured fold) and are stored on the compact record so later auto-folds still see them. Director rebuild (`/model`, interrupt, resume) restores those instructions from the latest compact record's parameters. An in-flight turn uses the same pair-safe compact-then-continue hop as threshold compact; an idle compact shows the fold and does not start a new turn.
- **Overflow recovery** — A `context_overflow` inference error would otherwise become a terminal error reply; the governor compacts and retries instead, bounded so a history the compactor cannot shrink does not loop forever. Overflow ignores hysteresis for the compact itself.

The TTL window is measured from the last `inference.done` (every inference rewrites the provider's prefix cache) with one fire per window per compact of any kind, and it never fires with a tool batch outstanding — stall pings can arrive mid-work. The consecutive-compact cap shared with the threshold path still bounds compact→infer→compact. Prompt-caching the summary call itself is an explicit non-goal: the fold carries no cache options.
Expand Down
2 changes: 1 addition & 1 deletion docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ src/
commands/
registry.ts Extensible slash-command registry
built-in.ts /help, /model, /settings, /permissions, /plugins,
/clear, /new, /mcp (connect providers from /model),
/clear, /new, /compact, /mcp (connect providers from /model),
/yolo
tui/
shell.ts Transcript, header, status line, prompt, overlays
Expand Down
2 changes: 1 addition & 1 deletion docs/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ the file path and parse details.

## Slash Commands (TUI)

The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** or `/connect` adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/mcp` (enable, disable, or remove servers), and `/yolo` (persists as the user-global skip-permissions default; `--dangerously-skip-permissions` still forces this process; secret-guard and authz still apply; `/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/<name>` command per available workflow. When a session starts with the persisted default already on, the TUI shows a startup notice ("Permission prompts are disabled by your saved default…") so the silent machine-wide default is never invisible; `corbits exec` prints the equivalent warning to stderr. Plugins can register additional commands.
The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** or `/connect` adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/compact` (fold conversation context now, optional trailing instructions to the summarizer; does not wait for the 60% occupancy governor; idle success shows the fold and does not start a new turn), `/mcp` (enable, disable, or remove servers), and `/yolo` (persists as the user-global skip-permissions default; `--dangerously-skip-permissions` still forces this process; secret-guard and authz still apply; `/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/<name>` command per available workflow. When a session starts with the persisted default already on, the TUI shows a startup notice ("Permission prompts are disabled by your saved default…") so the silent machine-wide default is never invisible; `corbits exec` prints the equivalent warning to stderr. Plugins can register additional commands.

**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`, `/lexicon`. Each one is a how-to playbook — the slash sends the skill body to the primary, which follows the steps. Skills do not assign identity or route the fleet; that stays on director system prompts. `/review` classifies the target first, then dispatches a selected fleet; `/pull-request-review` is worktree checkout plus a surface pass, loading `/review` for quality rules only; `/scribe` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` is the per-commit review/build/critique loop — it does not steal planning from `/plan`. Substantial Builder work consumes a counsel / `/plan` plan first; tiny parent-DIY stays plan-optional. `/plan` authors an eng change plan (files, AC, non-goals, risks, ordered steps) and does not implement. `/create-issue` remains the tracker command: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). `/lexicon` owns director-prompt drift and size against the agents repo at a pinned commit. There is no first-party dispatch skill — Skywalker orchestrates natively. `git-rebase`, `linear-issue-workflow`, `style`, `philosophy`, `native-integration`, `typescript`, `ponytail`, and `opsh` stay `use_skill` only (`user-invocable: false`). Bake-only bars such as `idiot-proof` and `native-runtime` are not slashes and are not listed for `use_skill`. Draper and emil are not slashes; they remain closed directors via `spawn_agent(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `skill_search` (descriptions) then `use_skill` (body). Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone.

Expand Down
147 changes: 147 additions & 0 deletions src/agent/compaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import type {
} from "@intx/types/runtime";
import {
COMPACTION_CONTINUATION_EVENT,
OPERATOR_COMPACT_REASON,
compactFloorNoopNotice,
createCompactionGovernor,
stickyExtraInstructionsFromRecords,
} from "./compaction.js";
import {
compactionResumeDeltaFor,
Expand Down Expand Up @@ -785,6 +788,150 @@ describe("compaction governor", () => {
]);
expect(continuations).toBe(1);
});

test("manual compact bypasses the occupancy governor on an idle session", () => {
const governor = createCompactionGovernor(undefined);
governor.noteInferenceDone(inferenceDone(1000), tenTurns);

expect(governor.requestManual("keep the auth discussion")).toBe("kick");
expect(governor.extraInstructions).toBe("keep the auth discussion");
expect(governor.requestManual("")).toBe("armed");
expect(governor.extraInstructions).toBe("keep the auth discussion");

const actions = governor.interceptIdleContinuation(
emptyMessage(),
capabilities,
);
expect(
actions?.some(
(a) =>
a.type === "compact" &&
"reason" in a &&
a.reason === OPERATOR_COMPACT_REASON,
),
).toBe(true);
expect(governor.resumeAfterCompact(emptyMessage())).toBe("meter");
expect(governor.resumeAfterCompact(emptyMessage())).toBeNull();
});

test("manual compact during a tool pause continues the in-flight turn", () => {
const governor = createCompactionGovernor(() => undefined);
governor.noteInferenceDone(inferenceDone(1000), tenTurns);

expect(governor.requestManual("", { inFlight: true })).toBe("armed");
const actions = governor.interceptActions(
toolDone(),
inferAction,
capabilities,
);
expect(
actions?.some(
(a) =>
a.type === "compact" &&
"reason" in a &&
a.reason === OPERATOR_COMPACT_REASON,
),
).toBe(true);
expect(actions?.some((a) => a.type === "infer")).toBe(false);
expect(governor.resumeAfterCompact(emptyMessage())).toBe("infer");
});

test("manual compact no-ops below the compactor floor", () => {
const governor = createCompactionGovernor(undefined);
governor.noteInferenceDone(inferenceDone(overThreshold), threeTurns);
expect(governor.requestManual("focus on tests")).toBe("noop");
expect(governor.extraInstructions).toBeUndefined();
expect(
governor.interceptIdleContinuation(emptyMessage(), capabilities),
).toBeNull();
});

test("manual compact hydrates from restored turns without a prior decide", () => {
const governor = createCompactionGovernor(undefined);
expect(governor.compactTurnCount).toBe(0);
expect(governor.requestManual("keep the auth discussion")).toBe("noop");
expect(
governor.requestManual("keep the auth discussion", { turns: tenTurns }),
).toBe("kick");
expect(governor.compactTurnCount).toBe(10);
expect(governor.extraInstructions).toBe("keep the auth discussion");
});

test("restoreExtraInstructions hydrates a new governor from a compact record", () => {
const written = {
strategy: "pruning-compactor",
version: "1",
parameters: { extraInstructions: "keep the auth discussion" },
reason: "compacted",
decisions: {},
};
expect(stickyExtraInstructionsFromRecords([written])).toBe(
"keep the auth discussion",
);

const rebuilt = createCompactionGovernor(undefined);
expect(rebuilt.extraInstructions).toBeUndefined();
rebuilt.restoreExtraInstructions(
stickyExtraInstructionsFromRecords([written]),
);
expect(rebuilt.extraInstructions).toBe("keep the auth discussion");
rebuilt.restoreExtraInstructions(undefined);
expect(rebuilt.extraInstructions).toBe("keep the auth discussion");
});

test("sticky extra instructions skip empty values and later non-pruning records", () => {
expect(
stickyExtraInstructionsFromRecords([
{
strategy: "other",
parameters: { extraInstructions: "ignore me" },
},
{
strategy: "pruning-compactor",
parameters: { extraInstructions: " keep tests " },
},
{
strategy: "pruning-compactor",
parameters: { extraInstructions: "older" },
},
]),
).toBe("keep tests");
expect(
stickyExtraInstructionsFromRecords([
{
strategy: "pruning-compactor",
parameters: { extraInstructions: "" },
},
{
strategy: "pruning-compactor",
parameters: { extraInstructions: 12 },
},
]),
).toBeUndefined();
});

test("second /compact during the apply-to-meter window does not double-fold", () => {
const governor = createCompactionGovernor(undefined);
governor.noteInferenceDone(inferenceDone(1000), tenTurns);
expect(governor.requestManual("keep the auth discussion")).toBe("kick");
expect(
governor.interceptIdleContinuation(emptyMessage(), capabilities),
).not.toBeNull();

expect(governor.requestManual("also keep the billing notes")).toBe("armed");
expect(governor.extraInstructions).toBe("also keep the billing notes");
expect(
governor.interceptIdleContinuation(emptyMessage(), capabilities),
).toBeNull();
expect(governor.resumeAfterCompact(emptyMessage())).toBe("meter");
});

test("noop /compact below the floor tells the operator instructions were not saved", () => {
expect(compactFloorNoopNotice("")).toBe("Nothing to compact yet.");
expect(compactFloorNoopNotice("keep the auth discussion")).toBe(
"Nothing to compact yet. Instructions were not saved.",
);
});
});

describe("provider-aware idle recompress (CL-8745)", () => {
Expand Down
Loading
Loading