Phase 10: rescan branch for tracked skills when state entry missing — SPEC §6.3.
Walk user messages and read tool calls on session_start to rebuild tracked skills from history when no skill-reinject:state entry exists on the branch. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+7
-2
@@ -22,6 +22,7 @@ import {
|
|||||||
tryConsumeDeferredReinject,
|
tryConsumeDeferredReinject,
|
||||||
} from "./reinject.js";
|
} from "./reinject.js";
|
||||||
import { readSettings } from "./settings.js";
|
import { readSettings } from "./settings.js";
|
||||||
|
import { rescanSkillsFromBranch } from "./rescan.js";
|
||||||
import { findRegisteredSkillByName, resolveRegisteredSkills } from "./skills-registry.js";
|
import { findRegisteredSkillByName, resolveRegisteredSkills } from "./skills-registry.js";
|
||||||
import {
|
import {
|
||||||
applyExtensionState,
|
applyExtensionState,
|
||||||
@@ -99,9 +100,13 @@ export default function skillReinject(pi: ExtensionAPI): void {
|
|||||||
|
|
||||||
pi.on("session_start", async (_event, ctx) => {
|
pi.on("session_start", async (_event, ctx) => {
|
||||||
detectAndCachePiAutoCompact(pi, runtime);
|
detectAndCachePiAutoCompact(pi, runtime);
|
||||||
readSettings(ctx);
|
const settings = readSettings(ctx);
|
||||||
const loaded = loadStateFromBranch(ctx.sessionManager.getBranch());
|
const branch = ctx.sessionManager.getBranch();
|
||||||
|
const loaded = loadStateFromBranch(branch);
|
||||||
applyExtensionState(state, loaded ?? createInitialState());
|
applyExtensionState(state, loaded ?? createInitialState());
|
||||||
|
if (!loaded) {
|
||||||
|
rescanSkillsFromBranch(state, branch, ctx.cwd, registeredSkills, settings.trackReadPaths);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
pi.on("session_before_compact", async () => {
|
pi.on("session_before_compact", async () => {
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { dirname } from "node:path";
|
||||||
|
import type { SessionEntry, Skill } from "@earendil-works/pi-coding-agent";
|
||||||
|
import {
|
||||||
|
detectSlashSkill,
|
||||||
|
matchReadPathToSkillWhenEnabled,
|
||||||
|
parseSkillBlocksFromText,
|
||||||
|
userMessageText,
|
||||||
|
} from "./detect.js";
|
||||||
|
import { findRegisteredSkillByName, resolveRegisteredSkills } from "./skills-registry.js";
|
||||||
|
import { trackSkill, type ExtensionState } from "./state.js";
|
||||||
|
|
||||||
|
function entrySeenAt(entry: SessionEntry): number {
|
||||||
|
const ts = Date.parse(entry.timestamp);
|
||||||
|
return Number.isFinite(ts) ? ts : Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPathsFromContent(content: unknown): string[] {
|
||||||
|
if (!Array.isArray(content)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const paths: string[] = [];
|
||||||
|
for (const part of content) {
|
||||||
|
if (!part || typeof part !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const block = part as { type?: string; name?: string; arguments?: Record<string, unknown> };
|
||||||
|
if (block.type !== "toolCall" || block.name !== "read") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const path = block.arguments?.path;
|
||||||
|
if (typeof path === "string") {
|
||||||
|
paths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full branch rescan when no persisted state entry exists (SPEC §6.3). */
|
||||||
|
export function rescanSkillsFromBranch(
|
||||||
|
state: ExtensionState,
|
||||||
|
branch: SessionEntry[],
|
||||||
|
cwd: string,
|
||||||
|
registeredSkills: readonly Skill[],
|
||||||
|
trackReadPaths: boolean,
|
||||||
|
): void {
|
||||||
|
const skills = resolveRegisteredSkills(cwd, registeredSkills);
|
||||||
|
|
||||||
|
for (const entry of branch) {
|
||||||
|
if (entry.type !== "message") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { message } = entry;
|
||||||
|
const seenAt = entrySeenAt(entry);
|
||||||
|
|
||||||
|
if (message.role === "user") {
|
||||||
|
const text = userMessageText(message.content);
|
||||||
|
|
||||||
|
const slashName = detectSlashSkill(text);
|
||||||
|
if (slashName) {
|
||||||
|
const skill = findRegisteredSkillByName(skills, slashName);
|
||||||
|
if (skill) {
|
||||||
|
trackSkill(state, {
|
||||||
|
name: skill.name,
|
||||||
|
filePath: skill.filePath,
|
||||||
|
baseDir: skill.baseDir,
|
||||||
|
source: "slash",
|
||||||
|
seenAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const block of parseSkillBlocksFromText(text)) {
|
||||||
|
const registered = findRegisteredSkillByName(skills, block.name);
|
||||||
|
trackSkill(state, {
|
||||||
|
name: block.name,
|
||||||
|
filePath: registered?.filePath ?? block.location,
|
||||||
|
baseDir: registered?.baseDir ?? dirname(block.location),
|
||||||
|
source: "skill-block",
|
||||||
|
seenAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.role === "assistant") {
|
||||||
|
for (const path of readPathsFromContent(message.content)) {
|
||||||
|
const matched = matchReadPathToSkillWhenEnabled(path, skills, trackReadPaths);
|
||||||
|
if (matched) {
|
||||||
|
trackSkill(state, {
|
||||||
|
name: matched.name,
|
||||||
|
filePath: matched.filePath,
|
||||||
|
baseDir: matched.baseDir,
|
||||||
|
source: "read",
|
||||||
|
seenAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user