Phase 12: recalculate pending on each compact — SPEC §16.6.

Every session_compact replans pendingReinject in defer mode; skipped reinject clears the queue unless manual compaction is waiting for the next user prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-17 13:15:13 +07:00
parent 502ca39b3e
commit 09619d9dd8
3 changed files with 71 additions and 13 deletions
+15 -13
View File
@@ -16,8 +16,8 @@ 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 {
applyPendingReinjectAfterCompact,
clearPendingReinjectOnUserPrompt, clearPendingReinjectOnUserPrompt,
enqueueDeferredReinjectFromCompact,
planReinject, planReinject,
sendImmediateReinjectAllFollowUp, sendImmediateReinjectAllFollowUp,
sendImmediateReinjectIdle, sendImmediateReinjectIdle,
@@ -91,34 +91,36 @@ export default function skillReinject(pi: ExtensionAPI): void {
function handleSessionCompact(event: SessionCompactEvent, ctx: ExtensionContext): void { function handleSessionCompact(event: SessionCompactEvent, ctx: ExtensionContext): void {
const settings = readSettings(ctx); const settings = readSettings(ctx);
const skills = resolveRegisteredSkills(ctx.cwd, registeredSkills);
const planned = planReinject(state, settings, ctx, event, skills);
const shouldReinject = consumeCompactionOnSessionCompact( const shouldReinject = consumeCompactionOnSessionCompact(
compactionRuntime, compactionRuntime,
state, state,
state.sessionOverride, state.sessionOverride,
settings, settings,
); );
const deliveryMode = resolveDeliveryMode(settings, runtime, state.sessionIntegrationOverride);
if (deliveryMode === "defer") {
applyPendingReinjectAfterCompact(state, compactionRuntime, shouldReinject, planned);
persistState();
return;
}
applyPendingReinjectAfterCompact(state, compactionRuntime, shouldReinject, planned);
if (!shouldReinject) { if (!shouldReinject) {
persistState(); persistState();
return; return;
} }
const skills = resolveRegisteredSkills(ctx.cwd, registeredSkills); if (planned.length === 0) {
const deliveryMode = resolveDeliveryMode(settings, runtime, state.sessionIntegrationOverride);
if (deliveryMode === "defer") {
enqueueDeferredReinjectFromCompact(state, settings, ctx, event, skills);
persistState();
return;
}
const skillNames = planReinject(state, settings, ctx, event, skills);
if (skillNames.length === 0) {
persistState(); persistState();
return; return;
} }
if (ctx.isIdle()) { if (ctx.isIdle()) {
sendImmediateReinjectIdle(pi, skillNames, state, settings, skills, ctx); sendImmediateReinjectIdle(pi, planned, state, settings, skills, ctx);
} else { } else {
sendImmediateReinjectAllFollowUp(pi, skillNames, state, settings, skills, ctx); sendImmediateReinjectAllFollowUp(pi, planned, state, settings, skills, ctx);
} }
persistState(); persistState();
} }
+19
View File
@@ -96,6 +96,25 @@ export function enqueueDeferredReinjectFromCompact(
); );
} }
/**
* Update pending queue after session_compact; each compact recalculates or clears (SPEC §16.6).
* Manual compaction with default settings keeps stale pending until the next user prompt.
*/
export function applyPendingReinjectAfterCompact(
state: ExtensionState,
compactionRuntime: CompactionRuntime,
shouldReinject: boolean,
planned: readonly string[],
): void {
if (shouldReinject) {
state.pendingReinject = [...planned];
return;
}
if (!compactionRuntime.clearPendingReinjectOnNextUserInput) {
state.pendingReinject = [];
}
}
/** Warn when re-injecting many skills; unlimited by default with soft warn above 3 (SPEC §15). */ /** Warn when re-injecting many skills; unlimited by default with soft warn above 3 (SPEC §15). */
export function maybeWarnManySkills( export function maybeWarnManySkills(
skillCount: number, skillCount: number,
+37
View File
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { createCompactionRuntime } from "../src/compaction";
import { applyPendingReinjectAfterCompact } from "../src/reinject";
import { createInitialState } from "../src/state";
describe("applyPendingReinjectAfterCompact", () => {
it("replaces pending with a new plan on each compact", () => {
const runtime = createCompactionRuntime();
const state = createInitialState();
state.pendingReinject = ["alpha"];
applyPendingReinjectAfterCompact(state, runtime, true, ["beta", "gamma"]);
expect(state.pendingReinject).toEqual(["beta", "gamma"]);
});
it("clears pending when reinject is skipped", () => {
const runtime = createCompactionRuntime();
const state = createInitialState();
state.pendingReinject = ["alpha"];
applyPendingReinjectAfterCompact(state, runtime, false, ["beta"]);
expect(state.pendingReinject).toEqual([]);
});
it("keeps stale pending when manual compaction scheduled a user-prompt clear", () => {
const runtime = createCompactionRuntime();
runtime.clearPendingReinjectOnNextUserInput = true;
const state = createInitialState();
state.pendingReinject = ["alpha"];
applyPendingReinjectAfterCompact(state, runtime, false, ["beta"]);
expect(state.pendingReinject).toEqual(["alpha"]);
});
});