From a81337c08e6d100a900a414243025bbc310da43a Mon Sep 17 00:00:00 2001 From: GRayHook Date: Wed, 17 Jun 2026 17:31:43 +0700 Subject: [PATCH] =?UTF-8?q?Phase=2014:=20defer=20reinject=20plan=20without?= =?UTF-8?q?=20registry=20at=20compaction=20=E2=80=94=20B-002=20stage=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/index.ts | 6 ++++- src/reinject.ts | 43 +++++++++++++++++++++++---------- test/b002-repro-pre-fix.test.ts | 30 ++++++++++++++++++++++- 3 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7378ec5..77bd302 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { import { applyPendingReinjectAfterCompact, clearPendingReinjectOnUserPrompt, + planDeferredReinject, planReinject, sendImmediateReinjectAllFollowUp, sendImmediateReinjectIdle, @@ -102,7 +103,6 @@ export default function skillReinject(pi: ExtensionAPI): void { const trackedNames = state.skills.map((skill) => skill.name); const keptEntries = getKeptEntries(branch, event.compactionEntry.firstKeptEntryId); const keptPresent = skillsPresentInKeptWindow(keptEntries, trackedNames); - const planned = planReinject(state, settings, ctx, event, skills); const shouldReinject = consumeCompactionOnSessionCompact( compactionRuntime, state, @@ -110,6 +110,10 @@ export default function skillReinject(pi: ExtensionAPI): void { settings, ); 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); notifyReinjectDiag( diff --git a/src/reinject.ts b/src/reinject.ts index 8799ff4..35d42f4 100644 --- a/src/reinject.ts +++ b/src/reinject.ts @@ -9,6 +9,7 @@ import { existsSync } from "node:fs"; import { expandSkill } from "./expand.js"; import { filterSkillsNeedingReinject, + filterSkillsNeedingReinjectByKept, getKeptEntries, skillsPresentInKeptWindow, } from "./kept.js"; @@ -57,6 +58,31 @@ export function registeredSkillNames(skills: readonly Pick[]): Re 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 { + 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). * `registeredSkills` comes from resourceLoader — ExtensionContext has no getSkills(); wired in index.ts. @@ -68,10 +94,7 @@ export function planReinject( compactionEvent: SessionCompactEvent, registeredSkills: readonly Pick[], ): string[] { - const branch = ctx.sessionManager.getBranch(); - const keptEntries = getKeptEntries(branch, compactionEvent.compactionEntry.firstKeptEntryId); - const trackedNames = state.skills.map((skill) => skill.name); - const keptPresent = skillsPresentInKeptWindow(keptEntries, trackedNames); + const keptPresent = keptPresentAfterCompaction(state, ctx, compactionEvent); return filterSkillsNeedingReinject( state.skills, keptPresent, @@ -82,18 +105,12 @@ export function planReinject( /** Defer path on session_compact: queue planned skills without sendUserMessage (SPEC §6.5.1, §16.2). */ export function enqueueDeferredReinjectFromCompact( state: ExtensionState, - settings: SkillReinjectSettings, + _settings: SkillReinjectSettings, ctx: ExtensionContext, compactionEvent: SessionCompactEvent, - registeredSkills: readonly Pick[], + _registeredSkills: readonly Pick[], ): void { - state.pendingReinject = planReinject( - state, - settings, - ctx, - compactionEvent, - registeredSkills, - ); + state.pendingReinject = planDeferredReinject(state, ctx, compactionEvent); } /** diff --git a/test/b002-repro-pre-fix.test.ts b/test/b002-repro-pre-fix.test.ts index 35cbfec..bac7700 100644 --- a/test/b002-repro-pre-fix.test.ts +++ b/test/b002-repro-pre-fix.test.ts @@ -1,5 +1,5 @@ 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 { createInitialState, trackSkill } from "../src/state.js"; @@ -67,4 +67,32 @@ describe("B-002 pre-fix filter hypothesis", () => { 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"]); + }); });