From 2059f6033b1875220986a3d79de54679d08227fc Mon Sep 17 00:00:00 2001 From: GRayHook Date: Wed, 17 Jun 2026 11:57:48 +0700 Subject: [PATCH] =?UTF-8?q?Phase=207:=20add=20defer=20inject=20on=20before?= =?UTF-8?q?=5Fagent=5Fstart=20=E2=80=94=20combined=20skill=20blocks,=20cle?= =?UTF-8?q?ar=20queue.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds one injected custom message from pendingReinject via expandSkill, clears the queue only after content is built successfully. Co-authored-by: Cursor --- src/reinject.ts | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/reinject.ts b/src/reinject.ts index febd92d..67761eb 100644 --- a/src/reinject.ts +++ b/src/reinject.ts @@ -1,8 +1,10 @@ import type { + BeforeAgentStartEventResult, ExtensionContext, SessionCompactEvent, Skill, } from "@earendil-works/pi-coding-agent"; +import { expandSkill } from "./expand.js"; import { filterSkillsNeedingReinject, getKeptEntries, @@ -11,6 +13,8 @@ import { import type { SkillReinjectSettings } from "./settings.js"; import type { ExtensionState } from "./state.js"; +export const DEFERRED_REINJECT_CUSTOM_TYPE = "skill-reinject:inject"; + /** Names still registered in resourceLoader (SPEC §5.2). */ export function registeredSkillNames(skills: readonly Pick[]): ReadonlySet { return new Set(skills.map((skill) => skill.name)); @@ -54,3 +58,60 @@ export function enqueueDeferredReinjectFromCompact( registeredSkills, ); } + +/** Combined skill-block user text for pending names in queue order (SPEC §5.3). */ +export function buildDeferredReinjectContent( + pendingNames: readonly string[], + state: ExtensionState, + settings: SkillReinjectSettings, + registeredSkills: readonly Pick[], +): string { + const registeredByName = new Map(registeredSkills.map((skill) => [skill.name, skill])); + const blocks: string[] = []; + for (const name of pendingNames) { + const tracked = state.skills.find((skill) => skill.name === name); + const registered = registeredByName.get(name); + if (!tracked || !registered) { + continue; + } + blocks.push( + expandSkill( + { + name: tracked.name, + filePath: registered.filePath, + baseDir: registered.baseDir, + }, + settings.suffix, + ), + ); + } + return blocks.join("\n\n"); +} + +/** + * Defer path on before_agent_start: inject one combined message, then clear queue (SPEC §6.5.1). + * Returns undefined when pendingReinject is empty. + */ +export function tryConsumeDeferredReinject( + state: ExtensionState, + settings: SkillReinjectSettings, + registeredSkills: readonly Pick[], +): BeforeAgentStartEventResult | undefined { + if (state.pendingReinject.length === 0) { + return undefined; + } + const pendingNames = [...state.pendingReinject]; + const content = buildDeferredReinjectContent(pendingNames, state, settings, registeredSkills); + if (!content) { + state.pendingReinject = []; + return undefined; + } + state.pendingReinject = []; + return { + message: { + customType: DEFERRED_REINJECT_CUSTOM_TYPE, + content, + display: true, + }, + }; +}