Phase 14: defer reinject plan without registry at compaction — B-002 stage 1

planDeferredReinject locks pending by kept-window only; defer path in index uses it while immediate keeps registered filter at compact time.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-17 17:31:43 +07:00
parent fe25e606b8
commit a81337c08e
3 changed files with 64 additions and 15 deletions
+5 -1
View File
@@ -23,6 +23,7 @@ import {
import { import {
applyPendingReinjectAfterCompact, applyPendingReinjectAfterCompact,
clearPendingReinjectOnUserPrompt, clearPendingReinjectOnUserPrompt,
planDeferredReinject,
planReinject, planReinject,
sendImmediateReinjectAllFollowUp, sendImmediateReinjectAllFollowUp,
sendImmediateReinjectIdle, sendImmediateReinjectIdle,
@@ -102,7 +103,6 @@ export default function skillReinject(pi: ExtensionAPI): void {
const trackedNames = state.skills.map((skill) => skill.name); const trackedNames = state.skills.map((skill) => skill.name);
const keptEntries = getKeptEntries(branch, event.compactionEntry.firstKeptEntryId); const keptEntries = getKeptEntries(branch, event.compactionEntry.firstKeptEntryId);
const keptPresent = skillsPresentInKeptWindow(keptEntries, trackedNames); const keptPresent = skillsPresentInKeptWindow(keptEntries, trackedNames);
const planned = planReinject(state, settings, ctx, event, skills);
const shouldReinject = consumeCompactionOnSessionCompact( const shouldReinject = consumeCompactionOnSessionCompact(
compactionRuntime, compactionRuntime,
state, state,
@@ -110,6 +110,10 @@ export default function skillReinject(pi: ExtensionAPI): void {
settings, settings,
); );
const deliveryMode = resolveDeliveryMode(settings, runtime, state.sessionIntegrationOverride); const deliveryMode = resolveDeliveryMode(settings, runtime, state.sessionIntegrationOverride);
const planned =
deliveryMode === "defer"
? planDeferredReinject(state, ctx, event)
: planReinject(state, settings, ctx, event, skills);
applyPendingReinjectAfterCompact(state, compactionRuntime, shouldReinject, planned); applyPendingReinjectAfterCompact(state, compactionRuntime, shouldReinject, planned);
notifyReinjectDiag( notifyReinjectDiag(
+30 -13
View File
@@ -9,6 +9,7 @@ import { existsSync } from "node:fs";
import { expandSkill } from "./expand.js"; import { expandSkill } from "./expand.js";
import { import {
filterSkillsNeedingReinject, filterSkillsNeedingReinject,
filterSkillsNeedingReinjectByKept,
getKeptEntries, getKeptEntries,
skillsPresentInKeptWindow, skillsPresentInKeptWindow,
} from "./kept.js"; } from "./kept.js";
@@ -57,6 +58,31 @@ export function registeredSkillNames(skills: readonly Pick<Skill, "name">[]): Re
return new Set(skills.map((skill) => skill.name)); return new Set(skills.map((skill) => skill.name));
} }
/** Kept-window skill names present after compaction (shared by plan helpers). */
function keptPresentAfterCompaction(
state: ExtensionState,
ctx: ExtensionContext,
compactionEvent: SessionCompactEvent,
): Set<string> {
const branch = ctx.sessionManager.getBranch();
const keptEntries = getKeptEntries(branch, compactionEvent.compactionEntry.firstKeptEntryId);
const trackedNames = state.skills.map((skill) => skill.name);
return skillsPresentInKeptWindow(keptEntries, trackedNames);
}
/**
* Defer planning at compaction: tracked skills absent from kept window only (SPEC §6.5.1).
* Registry filter runs later on before_agent_start (Phase 14 / B-002).
*/
export function planDeferredReinject(
state: ExtensionState,
ctx: ExtensionContext,
compactionEvent: SessionCompactEvent,
): string[] {
const keptPresent = keptPresentAfterCompaction(state, ctx, compactionEvent);
return filterSkillsNeedingReinjectByKept(state.skills, keptPresent);
}
/** /**
* Skill names to re-inject after compaction: tracked, absent from kept window, still registered (SPEC §5.2). * Skill names to re-inject after compaction: tracked, absent from kept window, still registered (SPEC §5.2).
* `registeredSkills` comes from resourceLoader — ExtensionContext has no getSkills(); wired in index.ts. * `registeredSkills` comes from resourceLoader — ExtensionContext has no getSkills(); wired in index.ts.
@@ -68,10 +94,7 @@ export function planReinject(
compactionEvent: SessionCompactEvent, compactionEvent: SessionCompactEvent,
registeredSkills: readonly Pick<Skill, "name">[], registeredSkills: readonly Pick<Skill, "name">[],
): string[] { ): string[] {
const branch = ctx.sessionManager.getBranch(); const keptPresent = keptPresentAfterCompaction(state, ctx, compactionEvent);
const keptEntries = getKeptEntries(branch, compactionEvent.compactionEntry.firstKeptEntryId);
const trackedNames = state.skills.map((skill) => skill.name);
const keptPresent = skillsPresentInKeptWindow(keptEntries, trackedNames);
return filterSkillsNeedingReinject( return filterSkillsNeedingReinject(
state.skills, state.skills,
keptPresent, keptPresent,
@@ -82,18 +105,12 @@ export function planReinject(
/** Defer path on session_compact: queue planned skills without sendUserMessage (SPEC §6.5.1, §16.2). */ /** Defer path on session_compact: queue planned skills without sendUserMessage (SPEC §6.5.1, §16.2). */
export function enqueueDeferredReinjectFromCompact( export function enqueueDeferredReinjectFromCompact(
state: ExtensionState, state: ExtensionState,
settings: SkillReinjectSettings, _settings: SkillReinjectSettings,
ctx: ExtensionContext, ctx: ExtensionContext,
compactionEvent: SessionCompactEvent, compactionEvent: SessionCompactEvent,
registeredSkills: readonly Pick<Skill, "name">[], _registeredSkills: readonly Pick<Skill, "name">[],
): void { ): void {
state.pendingReinject = planReinject( state.pendingReinject = planDeferredReinject(state, ctx, compactionEvent);
state,
settings,
ctx,
compactionEvent,
registeredSkills,
);
} }
/** /**
+29 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { planReinject } from "../src/reinject.js"; import { planDeferredReinject, planReinject } from "../src/reinject.js";
import { createDefaultSettings } from "../src/settings.js"; import { createDefaultSettings } from "../src/settings.js";
import { createInitialState, trackSkill } from "../src/state.js"; import { createInitialState, trackSkill } from "../src/state.js";
@@ -67,4 +67,32 @@ describe("B-002 pre-fix filter hypothesis", () => {
expect(planned).toEqual([]); expect(planned).toEqual([]);
}); });
it("defer plan includes skill absent from kept even when registered is empty", () => {
const state = createInitialState();
trackSkill(state, {
name: "fup-blame-commits",
filePath: "/home/user/.cursor/skills/fup-blame-commits/SKILL.md",
baseDir: "/home/user/.cursor/skills/fup-blame-commits",
source: "skill-block",
});
const branch = [
{
id: "keep-1",
type: "message",
message: { role: "user", content: "plain text after compact" },
},
] as never;
const planned = planDeferredReinject(
state,
{
sessionManager: { getBranch: () => branch },
} as never,
{ compactionEntry: { firstKeptEntryId: "keep-1" } } as never,
);
expect(planned).toEqual(["fup-blame-commits"]);
});
}); });