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>
This commit is contained in:
DietrichGebert 2026-07-07 01:23:31 +02:00 committed by GitHub
parent 40e50d9e03
commit 39bad58d40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 27 additions and 1 deletions

View File

@ -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)}` };
});
}

View File

@ -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({