Compare commits

...

2 Commits

Author SHA1 Message Date
Developer 5f1c107667 feat: avoid duplicate project-map hint injection by checking context
The before_agent_start handler now scans the active session context via
ctx.sessionManager.buildSessionContext() for an existing pi-project-map-hint
custom message and skips injection when one is already present in the
current branch. This prevents duplicate visible hints in advisory/pre-init
modes and duplicate hidden hints in strong/strict modes. The hint is
automatically re-injected after compaction or /tree navigation removes it
from the active path.

Also removes the unused hooks/on-prompt.ts prompt-text injector.
2026-06-14 08:57:23 +00:00
Developer 6bc7be4c21 feat: avoid duplicate project-map hint injection by checking context
The before_agent_start handler now inspects the current session context
and skips injection if a pi-project-map-hint custom message is already
present in the active branch. This prevents duplicate hints on every
prompt while still re-injecting after compaction or tree navigation.

Also removes the unused hooks/on-prompt.ts prompt-text injector.
2026-06-14 08:53:26 +00:00
4 changed files with 81 additions and 15 deletions
+2
View File
@@ -267,6 +267,8 @@ Payload fallback scanning is handled inside `context`-level decision logic; ther
`detectEditIntent()` and `detectArchitectureSensitiveReasoning()` provide heuristic fallback for generic turns.
In addition to marker-based deduplication, `before_agent_start` scans the active session context via `ctx.sessionManager.buildSessionContext()` for an existing `pi-project-map-hint` custom message. If one is already present in the current branch, the handler skips injection entirely. This prevents duplicate visible hints in advisory/pre-init modes and duplicate hidden hints in strong/strict modes when the session context already contains the guidance. The hint is automatically re-injected after compaction or `/tree` navigation removes it from the active path.
### 7.4 Protocol path and strict bypass
The **protocol path** is present when outgoing context contains:
-11
View File
@@ -1,11 +0,0 @@
// Pi skill prompt hook
// Injected into every prompt to remind the agent to maintain .pi-map.md files
export const MAINTENANCE_INSTRUCTION = `
If you modify any source file, run \`project-map:patch <file-path>\` to update the analysis.
If you suspect staleness, run \`project-map:validate\`.
`;
export function injectPrompt(originalPrompt: string): string {
return `${originalPrompt}\n\n---\n${MAINTENANCE_INSTRUCTION}`;
}
+29 -4
View File
@@ -81,6 +81,25 @@ function renderProgressBar(
return `[${bar}] ${completed}/${total}${file}`;
}
const HINT_CUSTOM_TYPE = "pi-project-map-hint";
function hintAlreadyInContext(ctx: any): boolean {
const manager = ctx?.sessionManager;
if (!manager || typeof manager.buildSessionContext !== "function") {
return false;
}
const { messages } = manager.buildSessionContext();
if (!Array.isArray(messages)) return false;
return messages.some(
(m: any) =>
m &&
m.role === "custom" &&
m.customType === HINT_CUSTOM_TYPE,
);
}
export default function (pi: ExtensionAPI) {
let lastRootPairMtimes: import("./src/index.js").RootPairMtimes = {};
pi.registerTool({
@@ -351,7 +370,9 @@ export default function (pi: ExtensionAPI) {
}
});
// Inject maintenance instructions before agent starts
// Inject maintenance instructions before agent starts, but only once
// within the current branch of context. Re-inject after compaction or
// tree navigation removes the hint from the active path.
pi.on("before_agent_start", async (_event, _ctx) => {
const config = loadConfig(_ctx.cwd);
const mapFiles = findPiMapFiles(_ctx.cwd);
@@ -366,9 +387,10 @@ export default function (pi: ExtensionAPI) {
if (!modeAllowsPreInitHint(config.promptInjectionMode)) {
return {};
}
if (hintAlreadyInContext(_ctx)) return {};
return {
message: {
customType: "pi-project-map-hint",
customType: HINT_CUSTOM_TYPE,
content: buildPreInitHint(),
display: true,
},
@@ -378,9 +400,10 @@ export default function (pi: ExtensionAPI) {
// Slice 4: advisory mode shows a visible lightweight reminder after init.
// No root-pair preload, no per-turn reinjection.
if (config.promptInjectionMode === "advisory") {
if (hintAlreadyInContext(_ctx)) return {};
return {
message: {
customType: "pi-project-map-hint",
customType: HINT_CUSTOM_TYPE,
content: buildAdvisoryReminder(),
display: true,
},
@@ -414,12 +437,14 @@ export default function (pi: ExtensionAPI) {
return {};
}
if (hintAlreadyInContext(_ctx)) return {};
// Slice 2: post-init root-pair preload + budgeted expansion
const contextWindow = discoverContextWindow(_ctx);
const payload = buildInjectionPayload(_ctx.cwd, config, contextWindow);
return {
message: {
customType: "pi-project-map-hint",
customType: HINT_CUSTOM_TYPE,
content: payload.content,
display: payload.display,
},
+50
View File
@@ -70,6 +70,9 @@ describe("pi-extension", () => {
})),
},
ui: { notify: mockNotify },
sessionManager: {
buildSessionContext: vi.fn(() => ({ messages: [] })),
},
};
const mockPi = {
@@ -505,6 +508,53 @@ describe("pi-extension", () => {
expect(result.message.content).toContain("Updated");
expect(result.message.content).toContain("TestIndex");
});
it("skips injection when hint is already present in session context", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
writeFileSync(
join(dir, ".pi-project-map.json"),
JSON.stringify({ promptInjectionMode: "advisory" }),
);
mockCtx.cwd = dir;
mockCtx.sessionManager.buildSessionContext = vi.fn(() => ({
messages: [
{
role: "custom",
customType: "pi-project-map-hint",
content: "existing hint",
display: false,
},
],
}));
const handler = registeredEvents.before_agent_start;
const result = await handler(null, mockCtx);
expect(result).toEqual({});
});
it("injects hint again when session context does not contain it", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
writeFileSync(
join(dir, ".pi-project-map.json"),
JSON.stringify({ promptInjectionMode: "advisory" }),
);
mockCtx.cwd = dir;
mockCtx.sessionManager.buildSessionContext = vi.fn(() => ({
messages: [
{ role: "user", content: "hello" },
{ role: "assistant", content: [{ type: "text", text: "hi" }] },
],
}));
const handler = registeredEvents.before_agent_start;
const result = await handler(null, mockCtx);
expect(result).toHaveProperty("message");
expect(result.message.customType).toBe("pi-project-map-hint");
});
});
describe("context event", () => {