From 39bad58d40a293b2b40f4d2b1bce2f3fdea1ca26 Mon Sep 17 00:00:00 2001 From: DietrichGebert Date: Tue, 7 Jul 2026 01:23:31 +0200 Subject: [PATCH] fix: guard pi before_agent_start against bad event and missing systemPrompt (#482) The before_agent_start handler read event.systemPrompt with no guard. A null/undefined event crashed the agent with a TypeError (#439), and a truthy event without a systemPrompt key stringified to the literal 'undefined', prepending garbage to every system prompt (#440). Both are fixed with one optional-chain guard that mirrors the event?. pattern the input handler already uses: prepend the base prompt only when present, otherwise inject the ruleset alone. Adds a regression test covering null/undefined event, missing systemPrompt, and the normal base-prompt path. Closes #439 Closes #440 Co-authored-by: Aroool <90670606+Aroool@users.noreply.github.com> --- pi-extension/index.js | 5 ++++- pi-extension/test/extension.test.js | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pi-extension/index.js b/pi-extension/index.js index 5f17627..f348fb0 100644 --- a/pi-extension/index.js +++ b/pi-extension/index.js @@ -184,6 +184,9 @@ export default function ponytailExtension(pi) { pi.on("before_agent_start", async (event) => { if (!currentMode || currentMode === "off") return; - return { systemPrompt: `${event.systemPrompt}\n\n${getPonytailInstructions(currentMode)}` }; + // Guard a null/undefined event or a missing systemPrompt: don't crash, and + // don't prepend the literal string "undefined" to the prompt (#439, #440). + const base = event?.systemPrompt ? `${event.systemPrompt}\n\n` : ""; + return { systemPrompt: `${base}${getPonytailInstructions(currentMode)}` }; }); } diff --git a/pi-extension/test/extension.test.js b/pi-extension/test/extension.test.js index b5f3918..2279ffb 100644 --- a/pi-extension/test/extension.test.js +++ b/pi-extension/test/extension.test.js @@ -77,6 +77,29 @@ test("/ponytail updates session mode and injects instructions", async () => with assert.ok(result.systemPrompt.includes("ultra")); })); +test("before_agent_start guards missing event and missing systemPrompt (#439, #440)", async () => withTempConfig(async () => { + const { events } = createPiHarness(); + const ctx = createCommandContext(); + await events.get("session_start")({ reason: "startup" }, ctx); // currentMode -> default (full) + + // #439: a null/undefined event must not crash, and still injects the ruleset. + for (const bad of [undefined, null]) { + const r = await events.get("before_agent_start")(bad, ctx); + assert.ok(r.systemPrompt.includes("PONYTAIL MODE ACTIVE")); + assert.ok(!r.systemPrompt.includes("undefined"), "must not contain the literal 'undefined'"); + } + + // #440: an event without a systemPrompt must not prepend the literal "undefined". + const empty = await events.get("before_agent_start")({}, ctx); + assert.ok(empty.systemPrompt.includes("PONYTAIL MODE ACTIVE")); + assert.ok(!empty.systemPrompt.startsWith("undefined"), "must not start with 'undefined'"); + + // A real base prompt is still preserved and prepended. + const withBase = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx); + assert.ok(withBase.systemPrompt.startsWith("BASE\n\n")); + assert.ok(withBase.systemPrompt.includes("PONYTAIL MODE ACTIVE")); +})); + test("session_start restores latest persisted mode", async () => withTempConfig(async () => { const { events } = createPiHarness(); const ctx = createCommandContext({