Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 21 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ async function generateExitSummary(ctx: ExtensionContext): Promise<ExitSummaryRe
const response = await complete(
model,
{ systemPrompt: EXIT_SUMMARY_SYSTEM_PROMPT, messages: summaryMessages },
{ apiKey, reasoningEffort: "low" },
{ apiKey, reasoningEffort: getExitSummaryReasoningEffort() },
);

const summaryText = response.content
Expand Down Expand Up @@ -537,6 +537,25 @@ export function getExitSummaryTimeoutMs(): number {
return Number.isInteger(configured) && configured > 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;
Expand Down Expand Up @@ -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()}`,
);

Expand Down
39 changes: 39 additions & 0 deletions test/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
ensureDirs,
ensureQmdEmbed,
forgetBlocks,
getExitSummaryReasoningEffort,
getExitSummaryTimeoutMs,
getQmdSearchTimeoutMs,
isExitSummaryEmpty,
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading