Files
pi-auto-reinject/src/reinject.ts
T
grayhook 2059f6033b Phase 7: add defer inject on before_agent_start — combined skill blocks, clear queue.
Builds one injected custom message from pendingReinject via expandSkill, clears the queue only after content is built successfully.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 11:57:48 +07:00

118 lines
3.5 KiB
TypeScript

import type {
BeforeAgentStartEventResult,
ExtensionContext,
SessionCompactEvent,
Skill,
} from "@earendil-works/pi-coding-agent";
import { expandSkill } from "./expand.js";
import {
filterSkillsNeedingReinject,
getKeptEntries,
skillsPresentInKeptWindow,
} from "./kept.js";
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<Skill, "name">[]): ReadonlySet<string> {
return new Set(skills.map((skill) => skill.name));
}
/**
* 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.
*/
export function planReinject(
state: ExtensionState,
_settings: SkillReinjectSettings,
ctx: ExtensionContext,
compactionEvent: SessionCompactEvent,
registeredSkills: readonly Pick<Skill, "name">[],
): 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);
return filterSkillsNeedingReinject(
state.skills,
keptPresent,
registeredSkillNames(registeredSkills),
);
}
/** Defer path on session_compact: queue planned skills without sendUserMessage (SPEC §6.5.1, §16.2). */
export function enqueueDeferredReinjectFromCompact(
state: ExtensionState,
settings: SkillReinjectSettings,
ctx: ExtensionContext,
compactionEvent: SessionCompactEvent,
registeredSkills: readonly Pick<Skill, "name">[],
): void {
state.pendingReinject = planReinject(
state,
settings,
ctx,
compactionEvent,
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<Skill, "name" | "filePath" | "baseDir">[],
): 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<Skill, "name" | "filePath" | "baseDir">[],
): 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,
},
};
}