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:
+10
-1
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user