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). */
|
/** Runtime compaction-source detection between input → before_compact → session_compact (SPEC §8). */
|
||||||
export interface CompactionRuntime {
|
export interface CompactionRuntime {
|
||||||
pendingCompactionSource: CompactionSource | null;
|
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 {
|
export function createCompactionRuntime(): CompactionRuntime {
|
||||||
return {
|
return {
|
||||||
pendingCompactionSource: null,
|
pendingCompactionSource: null,
|
||||||
|
clearPendingReinjectOnNextUserInput: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +54,14 @@ export function consumeCompactionOnSessionCompact(
|
|||||||
sessionOverride: boolean | null,
|
sessionOverride: boolean | null,
|
||||||
settings: SkillReinjectSettings,
|
settings: SkillReinjectSettings,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
const source = runtime.pendingCompactionSource;
|
||||||
const shouldReinject = shouldReinjectAfterCompaction(sessionOverride, settings, runtime);
|
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;
|
runtime.pendingCompactionSource = null;
|
||||||
return shouldReinject;
|
return shouldReinject;
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "./compaction.js";
|
} from "./compaction.js";
|
||||||
import { detectSlashSkill, matchReadPathToSkillWhenEnabled, parseSkillBlocksFromText, userMessageText } from "./detect.js";
|
import { detectSlashSkill, matchReadPathToSkillWhenEnabled, parseSkillBlocksFromText, userMessageText } from "./detect.js";
|
||||||
import {
|
import {
|
||||||
|
clearPendingReinjectOnUserPrompt,
|
||||||
enqueueDeferredReinjectFromCompact,
|
enqueueDeferredReinjectFromCompact,
|
||||||
planReinject,
|
planReinject,
|
||||||
sendImmediateReinjectAllFollowUp,
|
sendImmediateReinjectAllFollowUp,
|
||||||
@@ -147,7 +148,13 @@ export default function skillReinject(pi: ExtensionAPI): void {
|
|||||||
|
|
||||||
const settings = readSettings(ctx);
|
const settings = readSettings(ctx);
|
||||||
const pendingBefore = state.pendingReinject.length;
|
const pendingBefore = state.pendingReinject.length;
|
||||||
const deferred = tryConsumeDeferredReinject(state, settings, registeredSkills, ctx);
|
const deferred = tryConsumeDeferredReinject(
|
||||||
|
state,
|
||||||
|
settings,
|
||||||
|
registeredSkills,
|
||||||
|
ctx,
|
||||||
|
compactionRuntime,
|
||||||
|
);
|
||||||
if (pendingBefore > 0) {
|
if (pendingBefore > 0) {
|
||||||
persistState();
|
persistState();
|
||||||
}
|
}
|
||||||
@@ -163,6 +170,10 @@ export default function skillReinject(pi: ExtensionAPI): void {
|
|||||||
|
|
||||||
markManualCompactionFromInput(event.text, compactionRuntime);
|
markManualCompactionFromInput(event.text, compactionRuntime);
|
||||||
|
|
||||||
|
if (clearPendingReinjectOnUserPrompt(state, compactionRuntime)) {
|
||||||
|
persistState();
|
||||||
|
}
|
||||||
|
|
||||||
const skillName = detectSlashSkill(event.text);
|
const skillName = detectSlashSkill(event.text);
|
||||||
if (skillName) {
|
if (skillName) {
|
||||||
const skills = resolveRegisteredSkills(ctx.cwd, registeredSkills);
|
const skills = resolveRegisteredSkills(ctx.cwd, registeredSkills);
|
||||||
|
|||||||
+23
-1
@@ -13,6 +13,7 @@ import {
|
|||||||
skillsPresentInKeptWindow,
|
skillsPresentInKeptWindow,
|
||||||
} from "./kept.js";
|
} from "./kept.js";
|
||||||
import type { SkillReinjectSettings } from "./settings.js";
|
import type { SkillReinjectSettings } from "./settings.js";
|
||||||
|
import type { CompactionRuntime } from "./compaction.js";
|
||||||
import type { ExtensionState } from "./state.js";
|
import type { ExtensionState } from "./state.js";
|
||||||
|
|
||||||
export const DEFERRED_REINJECT_CUSTOM_TYPE = "skill-reinject:inject";
|
export const DEFERRED_REINJECT_CUSTOM_TYPE = "skill-reinject:inject";
|
||||||
@@ -173,16 +174,37 @@ export function reinjectNow(
|
|||||||
sendImmediateReinjectAllFollowUp(pi, skillNames, state, settings, registeredSkills, ctx);
|
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).
|
* 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(
|
export function tryConsumeDeferredReinject(
|
||||||
state: ExtensionState,
|
state: ExtensionState,
|
||||||
settings: SkillReinjectSettings,
|
settings: SkillReinjectSettings,
|
||||||
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
||||||
ctx?: ExtensionContext,
|
ctx?: ExtensionContext,
|
||||||
|
compactionRuntime?: CompactionRuntime,
|
||||||
): BeforeAgentStartEventResult | undefined {
|
): BeforeAgentStartEventResult | undefined {
|
||||||
|
if (compactionRuntime?.clearPendingReinjectOnNextUserInput) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (state.pendingReinject.length === 0) {
|
if (state.pendingReinject.length === 0) {
|
||||||
return undefined;
|
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