From 97435d54e473be8d4aa8089be4154c0f9647101c Mon Sep 17 00:00:00 2001 From: Dario Meloni Date: Mon, 24 Aug 2026 17:42:07 +0200 Subject: [PATCH] feat: make exit-summary reasoning effort configurable The exit-summary LLM call hardcoded reasoningEffort: "low". Some providers reject that value, silently breaking exit summaries: Baseten's GLM-5.2 only accepts high/max/none and returns HTTP 400 for "low", so the error is caught, the summary is null, and nothing is ever written to the daily log. Add PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT to override the effort (defaults to "low" for backward compatibility). Set it to a value the configured PI_MEMORY_EXIT_SUMMARY_MODEL accepts, or "off" to omit the parameter entirely and let the provider apply its own default. - Export getExitSummaryReasoningEffort() and use it in generateExitSummary - Surface the setting in memory_status - Document the new env var in the README config table - Add unit tests for default, pass-through, case-insensitivity, off, and empty-string handling Signed-off-by: Dario Meloni --- README.md | 1 + index.ts | 22 +++++++++++++++++++++- test/unit.test.ts | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 819156e..2927254 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,7 @@ This ensures in-progress context survives compaction and is visible in the next | `PI_MEMORY_SUMMARIZE_TRANSITIONS` | `1`, `true`, `yes`, `on` | unset | Also write exit summaries during lifecycle transitions (`/reload`, `/new`, `/resume`, `/fork`). By default these transitions skip summaries for speed. | | `PI_MEMORY_EXIT_SUMMARY` | `0`, `off`, `false`, `no` to disable | unset (enabled) | Disable the exit summary on real quit (Ctrl+D, `/quit`, session end). Quitting then does no LLM call and no `qmd update`, so it is instant; explicit `memory_write` during sessions is unaffected. | | `PI_MEMORY_EXIT_SUMMARY_MODEL` | `provider/model-id` | unset (session model) | Model used to write the exit summary, e.g. a cheaper/faster one. Unresolvable specs fall back to the session model with a warning. | +| `PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT` | `minimal`/`low`/`medium`/`high`/`xhigh`/`max`/`none`/`off` | `low` | Reasoning effort for the exit-summary LLM call. Some providers reject certain values (e.g. Baseten GLM-5.2 only accepts `high`/`max`/`none` and returns HTTP 400 for `low`, silently breaking exit summaries). Set this to a value your `PI_MEMORY_EXIT_SUMMARY_MODEL` accepts. Use `off` to omit the parameter and let the provider apply its own default. | | `PI_MEMORY_EXIT_SUMMARY_TIMEOUT_MS` | positive integer (milliseconds) | `10000` | Self-imposed timeout for exit-summary generation on quit. Pi awaits shutdown handlers with no timeout, so a hanging provider would otherwise block quitting indefinitely. On expiry nothing is persisted. | ## Troubleshooting diff --git a/index.ts b/index.ts index 3ad496d..29007bc 100644 --- a/index.ts +++ b/index.ts @@ -467,7 +467,7 @@ async function generateExitSummary(ctx: ExtensionContext): Promise 0 ? configured : DEFAULT_EXIT_SUMMARY_TIMEOUT_MS; } +const DEFAULT_EXIT_SUMMARY_REASONING_EFFORT = "low"; + +/** + * Reasoning effort passed to the exit-summary LLM call. Defaults to "low". + * + * Some providers reject certain efforts — e.g. Baseten's GLM-5.2 only accepts + * "high"/"max"/"none" and returns HTTP 400 for "low", silently breaking exit + * summaries (the error is caught, summary is null, nothing is persisted). + * Override with PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT to a value the + * configured PI_MEMORY_EXIT_SUMMARY_MODEL accepts. Set to "off" to omit the + * parameter entirely and let the provider apply its own default. + */ +export function getExitSummaryReasoningEffort(): string | undefined { + const value = (process.env.PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT ?? "").trim().toLowerCase(); + if (value === "off") return undefined; + if (value === "") return DEFAULT_EXIT_SUMMARY_REASONING_EFFORT; + return value; +} + export function shouldSkipExitSummaryForReason(reason: string | undefined): boolean { if (!reason) return false; if (shouldSummarizeLifecycleTransitions()) return false; @@ -2412,6 +2431,7 @@ export default function (pi: ExtensionAPI) { `- PI_MEMORY_DIR: ${process.env.PI_MEMORY_DIR ? "set" : "default"}`, `- PI_MEMORY_EXIT_SUMMARY: ${isExitSummaryEnabled() ? "enabled" : "disabled"}`, `- PI_MEMORY_EXIT_SUMMARY_MODEL: ${process.env.PI_MEMORY_EXIT_SUMMARY_MODEL?.trim() || "session model"}`, + `- PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT: ${getExitSummaryReasoningEffort() ?? "off"}`, `- PI_MEMORY_EXIT_SUMMARY_TIMEOUT_MS: ${getExitSummaryTimeoutMs()}`, ); diff --git a/test/unit.test.ts b/test/unit.test.ts index 79865a1..4d90f01 100644 --- a/test/unit.test.ts +++ b/test/unit.test.ts @@ -31,6 +31,7 @@ import { ensureDirs, ensureQmdEmbed, forgetBlocks, + getExitSummaryReasoningEffort, getExitSummaryTimeoutMs, getQmdSearchTimeoutMs, isExitSummaryEmpty, @@ -1790,6 +1791,44 @@ describe("lifecycle hooks", () => { }); }); + describe("exit summary reasoning effort", () => { + let savedEffort: string | undefined; + beforeEach(() => { + savedEffort = process.env.PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT; + }); + afterEach(() => { + if (savedEffort === undefined) delete process.env.PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT; + else process.env.PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT = savedEffort; + }); + + test("getExitSummaryReasoningEffort defaults to low", () => { + delete process.env.PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT; + expect(getExitSummaryReasoningEffort()).toBe("low"); + }); + + test("getExitSummaryReasoningEffort passes through configured values", () => { + for (const value of ["high", "max", "none", "medium", "minimal", "xhigh"]) { + process.env.PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT = value; + expect(getExitSummaryReasoningEffort()).toBe(value); + } + }); + + test("getExitSummaryReasoningEffort is case-insensitive", () => { + process.env.PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT = "HIGH"; + expect(getExitSummaryReasoningEffort()).toBe("high"); + }); + + test("getExitSummaryReasoningEffort returns undefined for off", () => { + process.env.PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT = "off"; + expect(getExitSummaryReasoningEffort()).toBeUndefined(); + }); + + test("getExitSummaryReasoningEffort treats empty string as default", () => { + process.env.PI_MEMORY_EXIT_SUMMARY_REASONING_EFFORT = " "; + expect(getExitSummaryReasoningEffort()).toBe("low"); + }); + }); + // -- session_before_compact -- test("session_before_compact appends handoff when scratchpad has open items", async () => {