Phase 12: manual compaction defer clear — SPEC §16.5, §12.3.

Stale pendingReinject from auto compaction is blocked until the next user prompt after manual /compact with default settings; a later auto compaction resets the clear flag.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-17 13:10:56 +07:00
parent 7ff7529957
commit c071f240d3
4 changed files with 120 additions and 3 deletions
+10 -1
View File
@@ -4,11 +4,14 @@ import type { CompactionSource, ExtensionState } from "./state.js";
/** Runtime compaction-source detection between input → before_compact → session_compact (SPEC §8). */
export interface CompactionRuntime {
pendingCompactionSource: CompactionSource | null;
/** Manual compaction with default settings: clear stale pending on next user prompt (SPEC §16.5, §12.3). */
clearPendingReinjectOnNextUserInput: boolean;
}
export function createCompactionRuntime(): CompactionRuntime {
return {
pendingCompactionSource: null,
clearPendingReinjectOnNextUserInput: false,
};
}
@@ -51,8 +54,14 @@ export function consumeCompactionOnSessionCompact(
sessionOverride: boolean | null,
settings: SkillReinjectSettings,
): boolean {
const source = runtime.pendingCompactionSource;
const shouldReinject = shouldReinjectAfterCompaction(sessionOverride, settings, runtime);
state.lastCompactionSource = runtime.pendingCompactionSource;
state.lastCompactionSource = source;
if (source === "manual" && !settings.reinjectOnManualCompaction) {
runtime.clearPendingReinjectOnNextUserInput = true;
} else if (source === "auto" && shouldReinject) {
runtime.clearPendingReinjectOnNextUserInput = false;
}
runtime.pendingCompactionSource = null;
return shouldReinject;
}
+12 -1
View File
@@ -16,6 +16,7 @@ import {
} from "./compaction.js";
import { detectSlashSkill, matchReadPathToSkillWhenEnabled, parseSkillBlocksFromText, userMessageText } from "./detect.js";
import {
clearPendingReinjectOnUserPrompt,
enqueueDeferredReinjectFromCompact,
planReinject,
sendImmediateReinjectAllFollowUp,
@@ -147,7 +148,13 @@ export default function skillReinject(pi: ExtensionAPI): void {
const settings = readSettings(ctx);
const pendingBefore = state.pendingReinject.length;
const deferred = tryConsumeDeferredReinject(state, settings, registeredSkills, ctx);
const deferred = tryConsumeDeferredReinject(
state,
settings,
registeredSkills,
ctx,
compactionRuntime,
);
if (pendingBefore > 0) {
persistState();
}
@@ -163,6 +170,10 @@ export default function skillReinject(pi: ExtensionAPI): void {
markManualCompactionFromInput(event.text, compactionRuntime);
if (clearPendingReinjectOnUserPrompt(state, compactionRuntime)) {
persistState();
}
const skillName = detectSlashSkill(event.text);
if (skillName) {
const skills = resolveRegisteredSkills(ctx.cwd, registeredSkills);
+23 -1
View File
@@ -13,6 +13,7 @@ import {
skillsPresentInKeptWindow,
} from "./kept.js";
import type { SkillReinjectSettings } from "./settings.js";
import type { CompactionRuntime } from "./compaction.js";
import type { ExtensionState } from "./state.js";
export const DEFERRED_REINJECT_CUSTOM_TYPE = "skill-reinject:inject";
@@ -173,16 +174,37 @@ export function reinjectNow(
sendImmediateReinjectAllFollowUp(pi, skillNames, state, settings, registeredSkills, ctx);
}
/**
* Manual `/compact` with default settings: drop stale pending on the next user prompt (SPEC §16.5, §12.3).
* Returns true when pending was cleared.
*/
export function clearPendingReinjectOnUserPrompt(
state: ExtensionState,
compactionRuntime: CompactionRuntime,
): boolean {
if (!compactionRuntime.clearPendingReinjectOnNextUserInput) {
return false;
}
compactionRuntime.clearPendingReinjectOnNextUserInput = false;
const hadPending = state.pendingReinject.length > 0;
state.pendingReinject = [];
return hadPending;
}
/**
* Defer path on before_agent_start: inject one combined message, then clear queue (SPEC §6.5.1).
* Returns undefined when pendingReinject is empty.
* Returns undefined when pendingReinject is empty or manual compaction scheduled a clear (SPEC §16.5).
*/
export function tryConsumeDeferredReinject(
state: ExtensionState,
settings: SkillReinjectSettings,
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
ctx?: ExtensionContext,
compactionRuntime?: CompactionRuntime,
): BeforeAgentStartEventResult | undefined {
if (compactionRuntime?.clearPendingReinjectOnNextUserInput) {
return undefined;
}
if (state.pendingReinject.length === 0) {
return undefined;
}
+75
View File
@@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest";
import {
consumeCompactionOnSessionCompact,
createCompactionRuntime,
} from "../src/compaction";
import { clearPendingReinjectOnUserPrompt, tryConsumeDeferredReinject } from "../src/reinject";
import { createDefaultSettings } from "../src/settings";
import { createInitialState } from "../src/state";
describe("manual compaction defer clear", () => {
it("schedules clear on manual compaction when reinjectOnManualCompaction is false", () => {
const runtime = createCompactionRuntime();
const state = createInitialState();
state.pendingReinject = ["alpha"];
runtime.pendingCompactionSource = "manual";
const shouldReinject = consumeCompactionOnSessionCompact(runtime, state, null, createDefaultSettings());
expect(shouldReinject).toBe(false);
expect(runtime.clearPendingReinjectOnNextUserInput).toBe(true);
expect(state.pendingReinject).toEqual(["alpha"]);
});
it("blocks deferred inject until user prompt clears stale pending", () => {
const runtime = createCompactionRuntime();
const state = createInitialState();
state.pendingReinject = ["alpha"];
runtime.clearPendingReinjectOnNextUserInput = true;
expect(
tryConsumeDeferredReinject(state, createDefaultSettings(), [], undefined, runtime),
).toBeUndefined();
expect(state.pendingReinject).toEqual(["alpha"]);
});
it("clears pending on user prompt and allows deferred inject again", () => {
const runtime = createCompactionRuntime();
const state = createInitialState();
state.pendingReinject = ["alpha"];
runtime.clearPendingReinjectOnNextUserInput = true;
expect(clearPendingReinjectOnUserPrompt(state, runtime)).toBe(true);
expect(state.pendingReinject).toEqual([]);
expect(runtime.clearPendingReinjectOnNextUserInput).toBe(false);
});
it("does not schedule clear when manual compaction may reinject", () => {
const runtime = createCompactionRuntime();
const state = createInitialState();
state.pendingReinject = ["alpha"];
runtime.pendingCompactionSource = "manual";
const settings = createDefaultSettings();
settings.reinjectOnManualCompaction = true;
const shouldReinject = consumeCompactionOnSessionCompact(runtime, state, true, settings);
expect(shouldReinject).toBe(true);
expect(runtime.clearPendingReinjectOnNextUserInput).toBe(false);
});
it("clears stale manual flag when a later auto compaction enqueues reinject", () => {
const runtime = createCompactionRuntime();
const state = createInitialState();
runtime.clearPendingReinjectOnNextUserInput = true;
state.pendingReinject = ["beta"];
runtime.pendingCompactionSource = "auto";
const shouldReinject = consumeCompactionOnSessionCompact(runtime, state, true, createDefaultSettings());
expect(shouldReinject).toBe(true);
expect(runtime.clearPendingReinjectOnNextUserInput).toBe(false);
expect(clearPendingReinjectOnUserPrompt(state, runtime)).toBe(false);
expect(state.pendingReinject).toEqual(["beta"]);
});
});