@vanillagreen/pi-qol 1.7.4 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # pi-qol
2
2
 
3
3
  ![QOL extension settings panel](https://raw.githubusercontent.com/vanillagreencom/vstack/main/pi-extensions/pi-qol/assets/settings-panel.png)
4
-
5
4
  ![Session search popup](https://raw.githubusercontent.com/vanillagreencom/vstack/main/pi-extensions/pi-qol/assets/session-search.gif)
6
5
  ![/context usage breakdown](https://raw.githubusercontent.com/vanillagreencom/vstack/main/pi-extensions/pi-qol/assets/context-usage.png)
7
6
 
@@ -12,11 +11,7 @@ Quality-of-life extension for Pi: compact statusline, multiline input, session n
12
11
  - Compact statusline with repo, branch, model, thinking level, and context percent; can be disabled.
13
12
  - Newline-insert in the editor (multi-line drafts without auto-submit), with a fallback binding for terminals that can't distinguish the primary key.
14
13
  - Sessions auto-name from your first prompt. `/rename` overrides anytime.
15
- - `/search` browses previous sessions with snippet previews; the configured shortcut opens it instantly.
16
14
  - Session search reads prompt snippets line-by-line, so very large session JSONL files do not have to be materialized just to browse, search, or import context.
17
- - `/context` shows a Claude-style context-window breakdown.
18
- - `/handoff <goal>` drafts a focused prompt for a new session.
19
- - `/schedule 20m <message>` or `/schedule 1h45m <message>` sends a delayed prompt without invoking the model until the timer fires.
20
15
  - Optional rate-limit auto-resume sends a configurable continuation after reset.
21
16
  - Permission gate prompts before risky `bash` commands. Default match: `rm -Rf`.
22
17
  - Notifications for ready, questions, blocked states, and task completion.
@@ -26,6 +21,8 @@ Quality-of-life extension for Pi: compact statusline, multiline input, session n
26
21
 
27
22
  ## Install
28
23
 
24
+ Requires Pi 0.80.4 or newer. Restart Pi after installation.
25
+
29
26
  Via [npm](https://www.npmjs.com/package/@vanillagreen/pi-qol):
30
27
 
31
28
  ```bash
@@ -39,8 +36,6 @@ cargo install --git https://github.com/vanillagreencom/vstack.git vstack
39
36
  vstack add vanillagreencom/vstack --pi-extension pi-qol --harness pi -y
40
37
  ```
41
38
 
42
- Restart Pi after installation.
43
-
44
39
  ## Commands
45
40
 
46
41
  | Command | Action |
@@ -48,166 +43,78 @@ Restart Pi after installation.
48
43
  | `/qol` | Open settings (or print status if extension-manager isn't installed). |
49
44
  | `/qol notify-test` | Send a test notification. |
50
45
  | `/rename [name]` | Set or show the current session's name. |
51
- | `/qol:rename` | Regenerate the session name from the first prompt. |
52
- | `/qol:rename:full` | Regenerate from the full conversation. |
53
- | `/context` | Show context-window usage with category breakdown. |
54
- | `/search [query]` | Open previous-session search. |
55
- | `/search:refresh` | Refresh the session search cache. |
56
- | `/handoff <goal>` | Draft a handoff prompt for a new session. |
46
+ | `/qol:rename` / `/qol:rename:full` | Regenerate the session name from the first prompt, or from the full conversation. |
47
+ | `/context` | Show a Claude-style context-window usage breakdown by category. |
48
+ | `/search [query]` | Open previous-session search with snippet previews; the configured shortcut opens it instantly. |
49
+ | `/search:refresh` | Refresh the session cache. |
50
+ | `/handoff <goal>` | Draft a focused handoff prompt for a new session. |
57
51
  | `/schedule <delay> <message>` | Send a user message after a timer without invoking the model now. Example: `/schedule 1h45m retry the previous request`. |
58
52
 
59
- Arguments support autocomplete.
60
-
61
- `/schedule` accepts `ms`, `s`, `m`, `h`, and `d` units; bare numbers mean minutes. Compact composite durations are accepted in largest-to-smallest order, like `1h45m`, `45m10s`, or `1h45m30s`. Pending prompts render above the statusline like steering/follow-up previews until they are sent or cancelled. Manage pending prompts with `/schedule list` and `/schedule cancel <id|all>`. Schedules are stored in the Pi session and re-armed on reload/resume; if Pi is not running at the due time, an overdue prompt sends when that session is next loaded.
53
+ Arguments support autocomplete. `/schedule` accepts `ms`, `s`, `m`, `h`, and `d` units; bare numbers mean minutes. Compact composite durations are accepted in largest-to-smallest order, like `1h45m`, `45m10s`, or `1h45m30s`. Pending prompts render above the statusline like steering/follow-up previews until they are sent or cancelled. Manage pending prompts with `/schedule list` and `/schedule cancel <id|all>`. Schedules are stored in the Pi session and re-armed on reload/resume; if Pi is not running at the due time, an overdue prompt sends when that session is next loaded.
62
54
 
63
55
  ## Settings
64
56
 
65
- Open `/extensions:settings`; settings appear under the **QOL** tab. Names below match the labels shown there.
66
-
67
- Project settings in `.pi/settings.json` apply only after Pi marks the workspace trusted; before trust, vstack Pi extensions read user/global settings only.
68
-
69
- Glyph style: each package exposes `glyphStyle` (`unicode` default, `ascii` for terminal-safe chrome). `@vanillagreen/pi-tool-renderer.globalGlyphStyleOverride=ascii` forces ASCII chrome across vstack Pi extensions while leaving tool/model/user content unchanged.
70
-
71
- ### Statusline
72
-
73
- | Setting | What it does |
74
- | --- | --- |
75
- | Enable QOL editor helpers | Master toggle for QOL statusline, commands, notifications, search, compaction, and editor helpers. |
76
- | Show compact statusline | Render or disable the QOL statusline row. |
77
- | Replace built-in footer | Hide Pi's default footer while the QOL statusline is enabled. |
78
- | Use π prompt editor | Use the compact prompt editor. |
79
- | Show session name title | Show the session name above the prompt and in the tmux pane title; refreshes as soon as Pi reports a session metadata change. |
80
- | Sync session name to tmux window name | Rename the tmux window to `π <session>`. |
81
- | Input bottom padding | Blank lines below the prompt. |
82
- | Show dirty marker | Append `*` to the branch when the worktree is dirty. |
83
-
84
- ### Input
85
-
86
- | Setting | What it does |
87
- | --- | --- |
88
- | shift+enter inserts newline | Insert a newline instead of submitting. |
89
- | Fallback newline key | Alternate binding for terminals that can't send the primary one. |
90
- | Style pending queue preview | Highlight Pi's pending-queue preview with a green left bar. |
91
- | Style image chips | Render `[Image #N]` placeholders as distinct chips. |
92
- | Show attachment count | Show a status badge when the draft has image placeholders. |
93
-
94
- ### Session
95
-
96
- | Setting | What it does |
97
- | --- | --- |
98
- | Enable /rename command | Register the `/rename` command. |
99
- | Enable /schedule command | Register `/schedule` for timer-based prompts, useful for retrying after rate limits reset. |
100
- | Auto-resume after rate limits | Send the configured continuation after a detected reset; cancels on newer turn. |
101
- | Auto-name new sessions | Generate a friendly session name from the first prompt. |
102
- | Auto-rename model | Model used for title generation. |
103
- | Auto-rename fallback model | Model tried when the primary fails. |
104
- | Deterministic fallback | Title-case words, truncated prompt, or none if all model calls fail. |
105
- | Auto-rename prefix | Optional static prefix on every generated name. |
106
- | Notify on auto-rename | Show a notification when auto-renaming. |
107
-
108
- Advanced: input cap, title length, output tokens, timeout, custom prompt template, and debug logging.
109
-
110
- ### Handoff
111
-
112
- | Setting | What it does |
113
- | --- | --- |
114
- | Enable /handoff command | Register the `/handoff` command. |
115
- | Review handoff prompt | Open an editor to edit the generated prompt before creating the session. |
116
-
117
- ### Context window
118
-
119
- | Setting | What it does |
120
- | --- | --- |
121
- | Enable /context command | Register `/context`. |
122
-
123
- `/context` also estimates the serialized payload size of the messages that would be sent on the next request. When that payload crosses **Transcript-risk warn budget (chars)** a `Transcript risk` block appears below the compact buffer section even if token count is still under the context window — useful for catching large blob-shaped tool outputs that inflate the request long before token count alone would page anyone. If transcript-risk estimation itself errors, `/context` shows a sanitized error in the same block rather than silently hiding the warning.
124
-
125
- ### Session search
126
-
127
- | Setting | What it does |
128
- | --- | --- |
129
- | Enable session search | Register `/search` and the overlay. |
130
- | Session search shortcut | Configurable; set to `none` to disable. |
131
- | Result limit | Max matching prompts returned. |
132
- | Visible session rows | Rows shown before scrolling; defaults to `8`. |
133
- | Preview snippets | Matching snippets shown on the preview screen. |
134
- | Session cache TTL | Seconds before the session list refreshes; `0` keeps it until you run `/search:refresh`. |
57
+ Open `/extensions:settings`; settings appear under the **QOL** tab. Names below match the labels shown there, grouped as the editor groups them. Project settings in `.pi/settings.json` apply only after Pi marks the workspace trusted; before trust, vstack Pi extensions read user/global settings only. Glyph style: each package exposes `glyphStyle` (`unicode` default, `ascii` for terminal-safe chrome). `@vanillagreen/pi-tool-renderer.globalGlyphStyleOverride=ascii` forces ASCII chrome across vstack Pi extensions while leaving tool/model/user content unchanged.
135
58
 
136
- Summary settings (model, max tokens, input cap) tune the summarizer when you import context from a previous session.
59
+ | Group | Setting | What it does |
60
+ | --- | --- | --- |
61
+ | Statusline | Enable QOL editor helpers | Master toggle for QOL statusline, commands, notifications, search, compaction, and editor helpers. |
62
+ | Statusline | Show compact statusline | Render or disable the QOL statusline row. |
63
+ | Statusline | Replace built-in footer | Hide Pi's default footer while the QOL statusline is enabled. |
64
+ | Statusline | Use π prompt editor | Use the compact prompt editor. |
65
+ | Statusline | Show session name title | Show the session name above the prompt and in the tmux pane title; refreshes as soon as Pi reports a session metadata change. |
66
+ | Statusline | Sync session name to tmux window name | Rename the tmux window to `π <session>`. |
67
+ | Statusline | Input bottom padding | Blank lines below the prompt. |
68
+ | Statusline | Show dirty marker | Append `*` to the branch when the worktree is dirty. |
69
+ | Input | shift+enter inserts newline | Insert a newline instead of submitting. |
70
+ | Input | Fallback newline key | Alternate binding for terminals that can't send the primary one. |
71
+ | Input | Style pending queue preview | Highlight Pi's pending-queue preview with a green left bar. |
72
+ | Input | Style image chips | Render `[Image #N]` placeholders as distinct chips. |
73
+ | Input | Show attachment count | Show a status badge when the draft has image placeholders. |
74
+ | Session, Handoff, Context window, Session search | Enable /rename, /schedule, /handoff, /context, session search | One toggle each: register that command. Session search also registers its overlay; `/schedule` is useful for retrying after rate limits reset. |
75
+ | Session | Auto-resume after rate limits | Send the configured continuation after a detected reset; cancels on newer turn. |
76
+ | Session | Auto-name new sessions | Generate a friendly session name from the first prompt. |
77
+ | Session | Auto-rename model / fallback model | Model used for title generation, and the model tried when the primary fails. |
78
+ | Session | Deterministic fallback | Title-case words, truncated prompt, or none if all model calls fail. |
79
+ | Session | Auto-rename prefix | Optional static prefix on every generated name. |
80
+ | Session | Notify on auto-rename | Show a notification when auto-renaming. |
81
+ | Handoff | Review handoff prompt | Open an editor to edit the generated prompt before creating the session. |
82
+ | Session search | Session search shortcut | Configurable; set to `none` to disable. |
83
+ | Session search | Result limit | Max matching prompts returned. |
84
+ | Session search | Visible session rows | Rows shown before scrolling; defaults to `8`. |
85
+ | Session search | Preview snippets | Matching snippets shown on the preview screen. |
86
+ | Session search | Session cache TTL | Seconds before the session list refreshes; `0` keeps it until you run `/search:refresh`. |
87
+ | Permission gate | Prompt before risky bash commands | Ask before bash commands matching the command list. Off by default; when enabled, non-interactive matches are blocked. |
88
+ | Permission gate | Commands to prompt for | Comma-separated literal fragments or `/regex/flags`. |
89
+ | Permission gate | Approval preview lines / characters | Cap the approval-prompt preview height and width. |
90
+ | Compaction | Custom compaction summaries | Use QOL summaries instead of Pi's default, with Pi-standard transient provider retries when available. |
91
+ | Compaction | Compaction model | Summarizer model. Defaults to `current`, meaning Pi's active model; set a provider/model when you want a dedicated larger-context summarizer. Thinking suffixes through `:max` are accepted, and Pi-resolved header/environment authentication is forwarded. |
92
+ | Compaction | Compaction detail profile | `concise`, `balanced`, or `exhaustive`. |
93
+ | Compaction | Include previous summary | Pass the previous summary for iterative continuity. |
94
+ | Compaction | Fallback to Pi default compaction | Run Pi's default compaction if QOL's fails. |
95
+ | Compaction | Show compaction notifications | Notify on compaction start/fail/complete. |
96
+ | Compaction | Custom branch summaries | Use QOL's chunked summarizer for `/tree` branch summaries; branch summaries do not write handoff artifacts. |
97
+ | Compaction | Remote compaction endpoint | Call a remote HTTP summarizer instead of a model. |
98
+ | Compaction | Idle compaction trigger | Auto-compact after the session sits idle above a token threshold. Idle thresholds (token threshold, idle delay, fixed token limit, percent limit) tune when it fires. |
99
+ | Long-session budget guard | Long-session budget guard | Master toggle for threshold-triggered bounded compaction. Default on. |
100
+ | Long-session budget guard | Budget guard percent / token limit | Context-window percentage that fires the guard (default `85`; `-1` disables percent-based firing), and the absolute token count that fires it (default `-1`, meaning percent only). |
101
+ | Long-session budget guard | Chunked compaction input cap | Max serialized characters per summarization request; long transcripts are chunked and summarized in bounded pieces so the compaction call itself cannot exceed provider buffer limits. `0` disables chunking. Default `240000`. |
102
+ | Long-session budget guard | Write pre-compaction handoff artifact | Write a pre-compaction handoff artifact (previous summary, last task state, referenced files and artifacts) for budget-guard and enabled custom session compactions; branch summaries never write one. Write failures surface as a QOL warning notification. Default on. |
103
+ | Long-session budget guard | Transcript-risk warn budget (chars) | `/context` warns when the serialized payload of messages-to-send exceeds this many characters, even if tokens are still below the context window. `0` disables. Default `600000`. |
104
+ | Thinking | Hidden thinking label | Label shown when thinking blocks are hidden. |
105
+ | Thinking | Show thinking timer | Show elapsed time next to collapsed `Thinking...` labels. |
106
+ | Thinking | Working indicator mode | `animated` ticks every 80ms; switch to `static` if you see flashes when the chat overflows. |
107
+
108
+ Advanced auto-rename settings (input cap, title length, output tokens, timeout, custom prompt template, debug logging) and session-search summary settings (model, max tokens, input cap — they tune the summarizer when you import context from a previous session) sit alongside the rows above.
109
+
110
+ `/context` also estimates the serialized payload size of the messages that would be sent on the next request. When that payload crosses **Transcript-risk warn budget (chars)** a `Transcript risk` block appears below the compact buffer section even if token count is still under the context window — useful for catching large blob-shaped tool outputs that inflate the request long before token count alone would page anyone.
137
111
 
138
112
  ### Notifications
139
113
 
140
- Master toggle: **Enable notifications**.
141
-
142
- Triggers (notify when): ready, direction needed, question popups, all tasks complete, critical/blocked.
143
-
144
- Channels: terminal bell, **Mute bell sound**, native terminal notifications (OSC 777/99 or Windows toast), tmux `display-message`, tmux window marking, OSC passthrough, and an optional in-Pi UI notice.
145
-
146
- Tuning: cooldown seconds, title, ready message, body length, tmux durations.
147
-
148
- Notes:
149
-
150
- - **Terminal notification protocol** picks between OSC 99 (Kitty) and OSC 777 automatically.
151
- - **Bell when tmux window active** is off so you don't get bells while looking at Pi.
152
- - **Mute bell sound** keeps notification routing enabled but suppresses QOL-emitted BEL bytes and uses ST terminators for OSC 777/99 where supported. Terminals or operating systems may still play their own sound for native notifications outside QOL control.
153
- - **tmux native via client TTY** sends OSC notifications to attached tmux clients so notifications still appear when the Pi window is inactive.
154
-
155
- Use `/qol notify-test` to verify your terminal/tmux setup, including silent behavior with **Mute bell sound** enabled.
156
-
157
- ### Permission gate
158
-
159
- | Setting | What it does |
160
- | --- | --- |
161
- | Prompt before risky bash commands | Ask before bash commands matching the command list. |
162
- | Commands to prompt for | Comma-separated literal fragments or `/regex/flags`. |
163
- | Approval preview lines | Cap the approval-prompt preview height. |
164
- | Approval preview characters | Cap the approval-prompt preview width. |
165
-
166
- Off by default. When enabled, non-interactive matches are blocked.
167
-
168
- ### Compaction
169
-
170
- | Setting | What it does |
171
- | --- | --- |
172
- | Custom compaction summaries | Use QOL summaries instead of Pi's default, with Pi-standard transient provider retries when available. |
173
- | Compaction model | Summarizer model. Defaults to `current`, meaning Pi's active model; set a provider/model when you want a dedicated larger-context summarizer. Thinking suffixes through `:max` are accepted, and Pi-resolved header/environment authentication is forwarded. |
174
- | Compaction detail profile | `concise`, `balanced`, or `exhaustive`. |
175
- | Include previous summary | Pass the previous summary for iterative continuity. |
176
- | Fallback to Pi default compaction | Run Pi's default compaction if QOL's fails. |
177
- | Show compaction notifications | Notify on compaction start/fail/complete. |
178
- | Custom branch summaries | Use the QOL summarizer for `/tree` branch summaries. |
179
- | Remote compaction endpoint | Call a remote HTTP summarizer instead of a model. |
180
- | Idle compaction trigger | Auto-compact after the session sits idle above a token threshold. |
181
-
182
- Idle thresholds (token threshold, idle delay, fixed token limit, percent limit) tune when idle compaction fires.
114
+ Master toggle: **Enable notifications**. Triggers (notify when): ready, direction needed, question popups, all tasks complete, critical/blocked. Channels: terminal bell, **Mute bell sound**, native terminal notifications (OSC 777/99 or Windows toast), tmux `display-message`, tmux window marking, OSC passthrough, and an optional in-Pi UI notice. Tuning: cooldown seconds, title, ready message, body length, tmux durations. **Terminal notification protocol** picks between OSC 99 (Kitty) and OSC 777 automatically. **Bell when tmux window active** is off so you don't get bells while looking at Pi. **Mute bell sound** keeps notification routing enabled but suppresses QOL-emitted BEL bytes and uses ST terminators for OSC 777/99 where supported; terminals or operating systems may still play their own sound for native notifications outside QOL control. **tmux native via client TTY** sends OSC notifications to attached tmux clients so notifications still appear when the Pi window is inactive. Use `/qol notify-test` to verify your terminal/tmux setup, including silent behavior with **Mute bell sound** enabled.
183
115
 
184
116
  ### Long-session budget guard
185
117
 
186
- For long autonomous runs the agent may not go idle, so idle compaction may never fire and the transcript can grow until provider/buffer limits hit. The budget guard runs on `agent_end` (not idle) and starts a compaction immediately when context usage crosses a percent of the model window or an absolute token limit. It fires once per threshold crossing — repeated `agent_end` events above the same threshold do not retrigger it, and the crossing key resets on a successful compaction or on a transient compaction failure (so the next `agent_end` retries).
187
-
188
- | Setting | What it does | Default |
189
- | --- | --- | --- |
190
- | Long-session budget guard | Master toggle for the agent_end budget guard. | on |
191
- | Budget guard percent | Context-window percentage that fires the guard. `-1` disables percent-based firing. | `85` |
192
- | Budget guard token limit | Absolute tokens that fire the guard. `-1` uses percent only. | `-1` |
193
- | Chunked compaction input cap | Max serialized characters per summarization request. Long transcripts are chunked, summarized chunk-by-chunk, then tree-reduced — every model/remote request (chunk + every reduce pass) is bounded so the compaction call itself cannot exceed provider buffer limits. `0` disables chunking. | `240000` |
194
- | Write pre-compaction handoff artifact | Before compaction, write `~/.pi/agent/vstack/sessions/<session>/pi-qol/handoff/<timestamp>.json` plus a `latest.json` pointer containing previous summary, last task state, and referenced files/artifacts. Write failures surface as a QOL warning notification and a `handoffArtifactError` field in the compaction details. | on |
195
- | Transcript-risk warn budget (chars) | `/context` shows a warning when the serialized payload of messages-to-send exceeds this many characters, even if tokens are still below the context window. `0` disables. | `600000` |
196
-
197
- When the budget guard fires it injects a sentinel into the compaction request so the QOL bounded handler always runs — chunked summarizer + handoff artifact — even if **Custom compaction summaries** is off. Manual compactions (`/tree`, idle compaction, user-triggered) still only use the QOL handler when **Custom compaction summaries** is on; otherwise they fall through to Pi's default compaction with no handoff artifact and no chunking. If you want every compaction to use the QOL bounded path, turn **Custom compaction summaries** on.
118
+ For long autonomous runs the agent may not go idle, so the transcript can grow until provider or buffer limits hit. The budget guard starts bounded compaction when context usage crosses a configured threshold, while giving Pi's built-in compaction priority and avoiding duplicate attempts. While it runs, QOL keeps a persistent status line above the prompt (and in the normal status footer when the compact statusline is disabled); after Pi prints the compacted-summary block the line changes to `QOL budget guard finalizing compaction…` until finalization completes, so long reload gaps do not look frozen.
198
119
 
199
- While budget-guard compaction is running, QOL keeps a persistent status line above the prompt (and in the normal status footer when the compact statusline is disabled). After Pi prints the compacted-summary block, the line changes to `QOL budget guard finalizing compaction…` until `ctx.compact()` reports completion, so long reload/finalization gaps do not look frozen.
200
-
201
- Recommended values for long autonomous runs:
202
-
203
- - Keep **Long-session budget guard** on. Lower **Budget guard percent** to `75` if your provider buffers are tight.
204
- - Optionally turn **Custom compaction summaries** on so user-initiated and idle compactions also use the QOL chunked summarizer + handoff artifact. (Budget-guard-triggered compactions always use them regardless of this setting.)
205
- - Lower **Chunked compaction input cap** to ~`120000` when the summarizer model has a small context window.
206
-
207
- ### Thinking
208
-
209
- | Setting | What it does |
210
- | --- | --- |
211
- | Hidden thinking label | Label shown when thinking blocks are hidden. |
212
- | Show thinking timer | Show elapsed time next to collapsed `Thinking...` labels. |
213
- | Working indicator mode | `animated` ticks every 80ms; switch to `static` if you see flashes when the chat overflows. |
120
+ Budget-guard compaction always uses QOL's chunked summarizer, even if **Custom compaction summaries** is off, and writes a pre-compaction handoff artifact only when **Write pre-compaction handoff artifact** is enabled. Manual/user-triggered and idle compactions use the QOL chunked path only when **Custom compaction summaries** is on, and follow the same handoff-artifact toggle. `/tree` branch summaries separately use QOL's chunked summarizer when **Custom branch summaries** is on, but do not write handoff artifacts. When the relevant summarization setting is off, Pi uses its default behavior for that compaction type. Recommended for long autonomous runs: keep **Long-session budget guard** on, and lower **Budget guard percent** to `75` if your provider buffers are tight. Optionally turn **Custom compaction summaries** on so user-initiated and idle compactions also use the QOL chunked summarizer — with **Write pre-compaction handoff artifact** enabled, those compactions also write handoff artifacts. Lower **Chunked compaction input cap** to ~`120000` when the summarizer model has a small context window.
@@ -1,7 +1,6 @@
1
- // Runtime dispatch for the agent_end budget guard. Owns the last-crossing
2
- // key and in-flight flag so the guard fires exactly once per threshold
3
- // crossing, validates ctx.compact before marking in-flight, and clears the
4
- // crossing key on failure so the next agent_end can retry.
1
+ // Runtime dispatch for the long-session budget guard. Agent-end observation and
2
+ // settled-time dispatch are separate so Pi's built-in post-agent compaction gets
3
+ // first refusal. The driver owns pending, satisfied, and in-flight trigger state.
5
4
 
6
5
  import { QOL_BUDGET_GUARD_SENTINEL, type BudgetTrigger } from "./budget-guard.js";
7
6
 
@@ -13,9 +12,9 @@ export interface GuardCompactOptions {
13
12
  onError?: (error: Error) => void;
14
13
  }
15
14
 
16
- export interface GuardDispatchInput {
17
- trigger: BudgetTrigger | undefined;
15
+ export interface GuardPendingDispatchInput {
18
16
  compact: ((options: GuardCompactOptions) => void) | undefined;
17
+ generation?: number;
19
18
  notify: (message: string, level: GuardLevel) => void;
20
19
  onStatus?: (message: string | undefined) => void;
21
20
  staleCtx?: () => boolean;
@@ -24,82 +23,197 @@ export interface GuardDispatchInput {
24
23
  export type DispatchOutcome =
25
24
  | { kind: "ignored" }
26
25
  | { kind: "no-trigger" }
26
+ | { kind: "staged"; reason: string }
27
27
  | { kind: "dedup" }
28
28
  | { kind: "in-flight" }
29
29
  | { kind: "no-compact-fn" }
30
30
  | { kind: "dispatched"; reason: string }
31
- | { kind: "dispatch-threw"; reason: string; error: string }
32
- | { kind: "complete" }
33
- | { kind: "failed"; error: string };
31
+ | { kind: "dispatch-threw"; reason: string; error: string };
32
+
33
+ export interface GuardDispatchResult {
34
+ outcome: DispatchOutcome;
35
+ completion: Promise<void>;
36
+ }
37
+
38
+ interface InFlightDispatch {
39
+ generation: number;
40
+ key: string;
41
+ sessionCompactVersion: number;
42
+ resolveCompletion: () => void;
43
+ }
44
+
45
+ interface OwnedTrigger {
46
+ generation: number;
47
+ trigger: BudgetTrigger;
48
+ }
49
+
50
+ interface SatisfiedTrigger {
51
+ generation: number;
52
+ key: string;
53
+ }
34
54
 
35
55
  export class BudgetGuardDriver {
36
- private lastKey: string | undefined;
37
- private inFlight = false;
56
+ private generation = 0;
57
+ private satisfiedTrigger: SatisfiedTrigger | undefined;
58
+ private pendingTrigger: OwnedTrigger | undefined;
59
+ private inFlight: InFlightDispatch | undefined;
60
+ private sessionCompactVersion = 0;
38
61
 
39
- reset(): void {
40
- this.lastKey = undefined;
41
- this.inFlight = false;
62
+ reset(): number {
63
+ this.inFlight?.resolveCompletion();
64
+ this.generation += 1;
65
+ this.satisfiedTrigger = undefined;
66
+ this.pendingTrigger = undefined;
67
+ this.inFlight = undefined;
68
+ this.sessionCompactVersion = 0;
69
+ return this.generation;
42
70
  }
43
71
 
44
- /** Returns true if the next call to dispatch is allowed to fire. Visible for tests. */
72
+ /** Returns true when no compaction call is currently active. Visible for tests. */
45
73
  get canFire(): boolean {
46
- return !this.inFlight;
74
+ return this.inFlight === undefined;
47
75
  }
48
76
 
49
77
  get currentKey(): string | undefined {
50
- return this.lastKey;
51
- }
52
-
53
- noteSessionCompacted(): void {
54
- // Successful compaction drops usage below the budget; let the next
55
- // crossing re-fire.
56
- this.lastKey = undefined;
57
- this.inFlight = false;
78
+ return this.inFlight?.key ?? this.pendingTrigger?.trigger.key ?? this.satisfiedTrigger?.key;
58
79
  }
59
80
 
60
- dispatch(input: GuardDispatchInput): DispatchOutcome {
61
- if (input.staleCtx?.()) return { kind: "ignored" };
81
+ /**
82
+ * Observe the trigger at agent_end without calling ctx.compact. Pi performs
83
+ * its built-in post-agent compaction check before agent_settled, so staging
84
+ * here prevents two compaction attempts from racing each other.
85
+ */
86
+ stage(trigger: BudgetTrigger | undefined, staleCtx?: () => boolean, generation = this.generation): DispatchOutcome {
87
+ if (generation !== this.generation) return { kind: "ignored" };
88
+ if (staleCtx?.()) return { kind: "ignored" };
62
89
  if (this.inFlight) return { kind: "in-flight" };
63
- const trigger = input.trigger;
64
90
  if (!trigger) {
65
- this.lastKey = undefined;
91
+ this.pendingTrigger = undefined;
92
+ this.satisfiedTrigger = undefined;
66
93
  return { kind: "no-trigger" };
67
94
  }
68
- if (trigger.key === this.lastKey) return { kind: "dedup" };
95
+ if (
96
+ (this.satisfiedTrigger?.generation === generation && trigger.key === this.satisfiedTrigger.key) ||
97
+ (this.pendingTrigger?.generation === generation && trigger.key === this.pendingTrigger.trigger.key)
98
+ ) {
99
+ return { kind: "dedup" };
100
+ }
101
+ this.pendingTrigger = { generation, trigger };
102
+ return { kind: "staged", reason: trigger.reason };
103
+ }
104
+
105
+ /**
106
+ * A completed Pi compaction satisfies whichever budget trigger is pending or
107
+ * in flight. Keep that key suppressed until usage produces no trigger or a
108
+ * genuinely different trigger key.
109
+ */
110
+ noteSessionCompacted(generation = this.generation): boolean {
111
+ if (generation !== this.generation) return false;
112
+ this.sessionCompactVersion += 1;
113
+ const currentKey = this.inFlight?.key ?? this.pendingTrigger?.trigger.key;
114
+ if (currentKey) this.satisfiedTrigger = { generation, key: currentKey };
115
+ this.pendingTrigger = undefined;
116
+ return true;
117
+ }
118
+
119
+ /** Dispatch a trigger previously staged by agent_end. */
120
+ dispatchPending(input: GuardPendingDispatchInput): GuardDispatchResult {
121
+ const completed = (outcome: DispatchOutcome): GuardDispatchResult => ({ completion: Promise.resolve(), outcome });
122
+ const generation = input.generation ?? this.generation;
123
+ if (generation !== this.generation) return completed({ kind: "ignored" });
124
+ if (input.staleCtx?.()) return completed({ kind: "ignored" });
125
+ if (this.inFlight) return completed({ kind: "in-flight" });
126
+ const pending = this.pendingTrigger;
127
+ if (!pending || pending.generation !== generation) return completed({ kind: "no-trigger" });
128
+ const trigger = pending.trigger;
129
+ if (this.satisfiedTrigger?.generation === generation && trigger.key === this.satisfiedTrigger.key) {
130
+ this.pendingTrigger = undefined;
131
+ return completed({ kind: "dedup" });
132
+ }
69
133
  if (typeof input.compact !== "function") {
70
134
  input.notify(`QOL budget guard cannot fire: ctx.compact is unavailable (${trigger.reason}).`, "warning");
71
- return { kind: "no-compact-fn" };
135
+ return completed({ kind: "no-compact-fn" });
72
136
  }
137
+
73
138
  input.notify(`QOL budget guard starting compaction: ${trigger.reason}`, "info");
74
139
  input.onStatus?.(`QOL budget guard compacting session: ${trigger.reason}`);
75
- this.inFlight = true;
76
- this.lastKey = trigger.key;
140
+ let resolveCompletion = () => {};
141
+ const completion = new Promise<void>((resolve) => {
142
+ resolveCompletion = resolve;
143
+ });
144
+ const dispatch: InFlightDispatch = {
145
+ generation,
146
+ key: trigger.key,
147
+ resolveCompletion,
148
+ sessionCompactVersion: this.sessionCompactVersion,
149
+ };
150
+ this.pendingTrigger = undefined;
151
+ this.inFlight = dispatch;
152
+ this.satisfiedTrigger = { generation, key: trigger.key };
77
153
  const instructions = `${QOL_BUDGET_GUARD_SENTINEL} QOL budget guard triggered at agent_end because ${trigger.reason}. Bound the summary input, preserve current task state, decisions, files, blockers, and next steps.`;
154
+
78
155
  try {
79
156
  input.compact({
80
157
  customInstructions: instructions,
81
158
  onComplete: () => {
82
- this.inFlight = false;
83
- input.onStatus?.(undefined);
84
- input.notify("QOL budget guard compaction completed.", "info");
159
+ if (this.generation !== dispatch.generation || this.inFlight !== dispatch) return;
160
+ try {
161
+ this.inFlight = undefined;
162
+ this.satisfiedTrigger = { generation: dispatch.generation, key: dispatch.key };
163
+ input.onStatus?.(undefined);
164
+ input.notify("QOL budget guard compaction completed.", "info");
165
+ } finally {
166
+ dispatch.resolveCompletion();
167
+ }
85
168
  },
86
169
  onError: (error: Error) => {
87
- this.inFlight = false;
88
- // Allow the next agent_end to retry rather than poisoning the
89
- // crossing key after a transient failure.
90
- this.lastKey = undefined;
91
- input.onStatus?.(undefined);
92
- input.notify(`QOL budget guard compaction failed: ${error.message}`, "error");
170
+ if (this.generation !== dispatch.generation || this.inFlight !== dispatch) return;
171
+ const duplicateCompletedAfterDispatch =
172
+ error.message === "Already compacted" &&
173
+ this.sessionCompactVersion > dispatch.sessionCompactVersion;
174
+ if (duplicateCompletedAfterDispatch) {
175
+ try {
176
+ this.inFlight = undefined;
177
+ this.satisfiedTrigger = { generation: dispatch.generation, key: dispatch.key };
178
+ input.onStatus?.(undefined);
179
+ } finally {
180
+ dispatch.resolveCompletion();
181
+ }
182
+ return;
183
+ }
184
+
185
+ try {
186
+ this.inFlight = undefined;
187
+ // A later session_compact already satisfied this key even when
188
+ // this callback reports a different, visible error.
189
+ if (this.sessionCompactVersion === dispatch.sessionCompactVersion) {
190
+ this.satisfiedTrigger = undefined;
191
+ }
192
+ input.onStatus?.(undefined);
193
+ input.notify(`QOL budget guard compaction failed: ${error.message}`, "error");
194
+ } finally {
195
+ dispatch.resolveCompletion();
196
+ }
93
197
  },
94
198
  });
95
- return { kind: "dispatched", reason: trigger.reason };
199
+ return { completion, outcome: { kind: "dispatched", reason: trigger.reason } };
96
200
  } catch (error) {
97
- this.inFlight = false;
98
- this.lastKey = undefined;
99
- input.onStatus?.(undefined);
201
+ if (this.generation === dispatch.generation && this.inFlight === dispatch) {
202
+ try {
203
+ this.inFlight = undefined;
204
+ this.satisfiedTrigger = undefined;
205
+ input.onStatus?.(undefined);
206
+ const message = error instanceof Error ? error.message : String(error);
207
+ input.notify(`QOL budget guard compaction failed to start: ${message}`, "error");
208
+ return { completion, outcome: { kind: "dispatch-threw", error: message, reason: trigger.reason } };
209
+ } finally {
210
+ dispatch.resolveCompletion();
211
+ }
212
+ }
213
+ dispatch.resolveCompletion();
100
214
  const message = error instanceof Error ? error.message : String(error);
101
- input.notify(`QOL budget guard compaction failed to start: ${message}`, "error");
102
- return { kind: "dispatch-threw", error: message, reason: trigger.reason };
215
+ return { completion, outcome: { kind: "dispatch-threw", error: message, reason: trigger.reason } };
103
216
  }
104
217
  }
218
+
105
219
  }
@@ -106,9 +106,10 @@ export function evaluateTranscriptRisk(input: TranscriptRiskInput): TranscriptRi
106
106
 
107
107
  /**
108
108
  * Sentinel marker injected into customInstructions when budget guard triggers
109
- * a compaction. Detected by handleQolCompaction to force the bounded QOL path
110
- * + handoff artifact write regardless of the global compaction.customEnabled
111
- * setting.
109
+ * a compaction. Detected by handleQolCompaction to force bounded QOL
110
+ * summarization regardless of compaction.customEnabled. The same path invokes
111
+ * the handoff writer, whose artifact creation obeys the independent
112
+ * compaction.handoffArtifactEnabled toggle.
112
113
  */
113
114
  export const QOL_BUDGET_GUARD_SENTINEL = "[QOL_BUDGET_GUARD]";
114
115
 
@@ -256,9 +256,9 @@ export function writeBudgetHandoffArtifact(ctx: ExtensionContext, handoff: QolBu
256
256
 
257
257
  export async function handleQolCompaction(event: any, ctx: ExtensionContext): Promise<any> {
258
258
  const isBudgetGuard = isBudgetGuardCompaction(event?.customInstructions);
259
- // Budget-guard-triggered compactions force the QOL bounded path so the
260
- // chunked summarizer + handoff artifact always run, even when the user
261
- // has not flipped compaction.customEnabled on.
259
+ // Budget-guard-triggered compactions force the QOL bounded summarizer even
260
+ // when compaction.customEnabled is off. The handoff writer still honors its
261
+ // independent compaction.handoffArtifactEnabled toggle.
262
262
  if (!isBudgetGuard && !settingBoolean("compaction.customEnabled", false, ctx.cwd)) return undefined;
263
263
  const preparation = event.preparation ?? {};
264
264
  const messages = [...(preparation.messagesToSummarize ?? []), ...(preparation.turnPrefixMessages ?? [])];
@@ -372,10 +372,10 @@ export type BudgetGuardTrigger = BudgetTrigger;
372
372
  export type TranscriptRiskState = TranscriptRiskResult;
373
373
 
374
374
  /**
375
- * Budget guard fires on agent_end (no idle wait) when context usage crosses a
376
- * percent of the model window or an absolute token limit. Returns a stable key
377
- * per crossing so the caller can suppress repeated triggers while usage stays
378
- * above the threshold.
375
+ * Budget guard evaluates on agent_end (no idle wait) when context usage crosses
376
+ * a percent of the model window or an absolute token limit. Returns a stable
377
+ * key per crossing so the caller can stage settled-time dispatch and suppress
378
+ * repeated triggers while usage stays above the threshold.
379
379
  */
380
380
  export function budgetGuardTrigger(ctx: ExtensionContext): BudgetGuardTrigger | undefined {
381
381
  if (!settingBoolean("compaction.budgetGuardEnabled", true, ctx.cwd)) return undefined;
@@ -39,9 +39,10 @@ export const DEFAULT_IDLE_COMPACTION_THRESHOLD_TOKENS = 200000;
39
39
  export const DEFAULT_IDLE_COMPACTION_SECONDS = 300;
40
40
 
41
41
  // Budget guard: catches long autonomous sessions before they hit provider/buffer
42
- // limits. Fires on agent_end (no idle wait) when usage crosses a percent or
43
- // absolute token threshold. One notification + one compaction trigger per
44
- // threshold crossing so it does not loop.
42
+ // limits. Evaluates on agent_end (no idle wait) when usage crosses a percent
43
+ // or absolute token threshold, then dispatches after Pi settles. One
44
+ // notification + one compaction trigger per threshold crossing so it does not
45
+ // race Pi's built-in post-agent compaction or loop.
45
46
  export const DEFAULT_BUDGET_GUARD_PERCENT = 85;
46
47
  export const DEFAULT_BUDGET_GUARD_TOKENS = -1;
47
48
 
@@ -22,7 +22,7 @@ type AutoRenameFallbackMode = "none" | "truncate" | "words";
22
22
  interface AutoRenameAuth {
23
23
  apiKey?: string;
24
24
  env?: Record<string, string>;
25
- headers?: Record<string, string>;
25
+ headers?: Record<string, string | null>;
26
26
  label: string;
27
27
  model: any;
28
28
  source: string;
@@ -237,10 +237,13 @@ function autoRenameModelSettings(cwd?: string): string[] {
237
237
  .filter((value, index, all) => all.findIndex((candidate) => candidate.toLowerCase() === value.toLowerCase()) === index);
238
238
  }
239
239
 
240
- function headerRecord(headers: unknown): Record<string, string> | undefined {
240
+ // Pi resolves provider headers as `ProviderHeaders` (`Record<string, string | null>`).
241
+ // A `null` value is a header-deletion marker that pi-ai acts on, so it must survive to
242
+ // the stream unchanged — dropping it silently re-sends the header we were told to remove.
243
+ export function headerRecord(headers: unknown): Record<string, string | null> | undefined {
241
244
  if (!headers || typeof headers !== "object" || Array.isArray(headers)) return undefined;
242
245
  const entries = Object.entries(headers as Record<string, unknown>)
243
- .filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[1].length > 0);
246
+ .filter((entry): entry is [string, string | null] => entry[1] === null || (typeof entry[1] === "string" && entry[1].length > 0));
244
247
  return entries.length > 0 ? Object.fromEntries(entries) : undefined;
245
248
  }
246
249
 
package/extensions/qol.ts CHANGED
@@ -264,6 +264,7 @@ export default function qol(pi: ExtensionAPI): void {
264
264
 
265
265
  const budgetGuardDriver = new BudgetGuardDriver();
266
266
  let budgetGuardStatus: string | undefined;
267
+ let budgetGuardSession: { generation: number; sessionManager: unknown } | undefined;
267
268
 
268
269
  const setBudgetGuardStatus = (ctx: ExtensionContext, message: string | undefined) => {
269
270
  budgetGuardStatus = message;
@@ -273,12 +274,32 @@ export default function qol(pi: ExtensionAPI): void {
273
274
  }
274
275
  };
275
276
 
276
- const resetBudgetGuard = () => {
277
+ const budgetGuardGenerationForContext = (ctx: ExtensionContext): number | undefined => {
278
+ if (!budgetGuardSession) return undefined;
279
+ try {
280
+ return ctx.sessionManager === budgetGuardSession.sessionManager ? budgetGuardSession.generation : undefined;
281
+ } catch (error) {
282
+ if (isStaleCtxError(error)) return undefined;
283
+ throw error;
284
+ }
285
+ };
286
+
287
+ const startBudgetGuardSession = (ctx: ExtensionContext) => {
288
+ const generation = budgetGuardDriver.reset();
289
+ budgetGuardSession = { generation, sessionManager: ctx.sessionManager };
290
+ budgetGuardStatus = undefined;
291
+ };
292
+
293
+ const resetBudgetGuard = (ctx: ExtensionContext) => {
294
+ if (budgetGuardGenerationForContext(ctx) === undefined) return;
277
295
  budgetGuardDriver.reset();
296
+ budgetGuardSession = undefined;
278
297
  budgetGuardStatus = undefined;
279
298
  };
280
299
 
281
- const maybeFireBudgetGuard = (ctx: ExtensionContext) => {
300
+ const stageBudgetGuard = (ctx: ExtensionContext) => {
301
+ const generation = budgetGuardGenerationForContext(ctx);
302
+ if (generation === undefined) return;
282
303
  let trigger;
283
304
  try {
284
305
  trigger = budgetGuardTrigger(ctx);
@@ -286,6 +307,12 @@ export default function qol(pi: ExtensionAPI): void {
286
307
  if (isStaleCtxError(error)) return;
287
308
  throw error;
288
309
  }
310
+ budgetGuardDriver.stage(trigger, undefined, generation);
311
+ };
312
+
313
+ const fireStagedBudgetGuard = async (ctx: ExtensionContext) => {
314
+ const generation = budgetGuardGenerationForContext(ctx);
315
+ if (generation === undefined) return;
289
316
  const notifySafely = (message: string, level: "info" | "warning" | "error") => {
290
317
  try {
291
318
  if (ctx.hasUI && settingBoolean("compaction.notify", true, ctx.cwd)) ctx.ui.notify(message, level);
@@ -294,12 +321,15 @@ export default function qol(pi: ExtensionAPI): void {
294
321
  throw error;
295
322
  }
296
323
  };
297
- budgetGuardDriver.dispatch({
324
+ const dispatch = budgetGuardDriver.dispatchPending({
298
325
  compact: typeof ctx.compact === "function" ? ctx.compact.bind(ctx) : undefined,
326
+ generation,
299
327
  notify: notifySafely,
300
328
  onStatus: (message) => setBudgetGuardStatus(ctx, message),
301
- trigger,
302
329
  });
330
+ // Supported Pi hosts always route ctx.compact terminal success/failure to
331
+ // one callback. Await it so Pi cannot emit terminal settlement first.
332
+ await dispatch.completion;
303
333
  };
304
334
 
305
335
  const scheduleIdleCompaction = (ctx: ExtensionContext) => {
@@ -565,7 +595,7 @@ export default function qol(pi: ExtensionAPI): void {
565
595
  subscribeCavemanBridge();
566
596
  latestSystemPromptOptions = undefined;
567
597
  resetAutoRename();
568
- resetBudgetGuard();
598
+ startBudgetGuardSession(ctx);
569
599
  resetThinkingTimer(ctx);
570
600
  const scheduleEnabled = settingBoolean("enableScheduleCommand", true, ctx.cwd);
571
601
  const rateLimitAutoResumeEnabled = rateLimitAutoResumeController.enabled(ctx);
@@ -639,7 +669,7 @@ export default function qol(pi: ExtensionAPI): void {
639
669
  scheduleController.clearTimers();
640
670
  rateLimitAutoResumeController.clearTimers();
641
671
  resetAutoRename();
642
- resetBudgetGuard();
672
+ resetBudgetGuard(ctx);
643
673
  clearIdleCompactionTimer();
644
674
  clearQuestionSubscribeTimer();
645
675
  if (sessionSearchWarmupTimer) clearTimeout(sessionSearchWarmupTimer);
@@ -649,7 +679,6 @@ export default function qol(pi: ExtensionAPI): void {
649
679
  questionUnsubscribe?.();
650
680
  questionUnsubscribe = undefined;
651
681
  currentCtx = undefined;
652
- budgetGuardStatus = undefined;
653
682
  const host = globalThis as unknown as Record<PropertyKey, unknown>;
654
683
  if (host[QOL_NOTIFICATION_SERVICE_SYMBOL] === notificationService) delete host[QOL_NOTIFICATION_SERVICE_SYMBOL];
655
684
  ctx.ui.setStatus(STATUS_KEY, undefined);
@@ -720,7 +749,7 @@ export default function qol(pi: ExtensionAPI): void {
720
749
  requestRender();
721
750
  }
722
751
  rateLimitAutoResumeController.noteAgentEnd(event, ctx);
723
- maybeFireBudgetGuard(ctx);
752
+ stageBudgetGuard(ctx);
724
753
  scheduleIdleCompaction(ctx);
725
754
  void attemptAutoRename(ctx);
726
755
  const text = lastAssistantTextFromAgentEnd(event, ctx);
@@ -743,11 +772,16 @@ export default function qol(pi: ExtensionAPI): void {
743
772
  }
744
773
  sendQolNotification(ctx, "ready", settingString("notification.readyMessage", "Ready for input", ctx.cwd), "info", "ready");
745
774
  });
775
+ pi.on("agent_settled", async (_event, ctx) => {
776
+ await fireStagedBudgetGuard(ctx);
777
+ });
746
778
  pi.on("session_compact", (_event, ctx) => {
779
+ const generation = budgetGuardGenerationForContext(ctx);
780
+ if (generation === undefined) return;
747
781
  if (budgetGuardStatus) setBudgetGuardStatus(ctx, "QOL budget guard finalizing compaction…");
748
- // After a successful compaction usage drops below the budget, so reset the
749
- // crossing key so the next threshold crossing re-fires the guard.
750
- budgetGuardDriver.noteSessionCompacted();
782
+ // Pi core can compact after agent_end but before agent_settled. Mark the
783
+ // staged or in-flight trigger satisfied so the same crossing cannot refire.
784
+ budgetGuardDriver.noteSessionCompacted(generation);
751
785
  if (!ctx.hasUI) return;
752
786
  void refreshStatusline(ctx);
753
787
  requestRender();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillagreen/pi-qol",
3
- "version": "1.7.4",
3
+ "version": "1.8.0",
4
4
  "description": "Pi quality-of-life extension: compact statusline/π prompt, reliable multiline input, styled pasted-image chips, session naming/search/context import, scheduled prompts, handoff, permission prompts, notifications, custom compaction, and a collapsed-thinking timer.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -875,7 +875,7 @@
875
875
  {
876
876
  "key": "compaction.budgetGuardEnabled",
877
877
  "label": "Long-session budget guard",
878
- "description": "Trigger compaction on agent_end (no idle wait) when context usage crosses budgetPercent or budgetTokens. Designed for long autonomous runs so transcript growth cannot silently hit provider/buffer limits. Fires once per threshold crossing, not in a loop.",
878
+ "description": "Start bounded compaction when an active session crosses the configured context budget, while giving Pi's built-in compaction priority. Designed for long autonomous runs so transcript growth cannot silently hit provider or buffer limits. Fires once per threshold crossing, not in a loop.",
879
879
  "type": "boolean",
880
880
  "default": true,
881
881
  "category": "Compaction Budget Guard",
@@ -884,7 +884,7 @@
884
884
  {
885
885
  "key": "compaction.budgetPercent",
886
886
  "label": "Budget guard percent",
887
- "description": "Context-window percentage that fires the budget guard at agent_end. Set -1 to disable percent-based firing and rely on absolute tokens instead.",
887
+ "description": "Context-window percentage that triggers the budget guard. Set -1 to disable percent-based triggering and rely on the absolute token limit instead.",
888
888
  "type": "number",
889
889
  "default": 85,
890
890
  "category": "Compaction Budget Guard",
@@ -893,7 +893,7 @@
893
893
  {
894
894
  "key": "compaction.budgetTokens",
895
895
  "label": "Budget guard token limit",
896
- "description": "Absolute token count above which the budget guard fires at agent_end. Set -1 to use only budgetPercent.",
896
+ "description": "Absolute token count that triggers the budget guard. Set -1 to use only the percentage threshold.",
897
897
  "type": "number",
898
898
  "default": -1,
899
899
  "category": "Compaction Budget Guard",
@@ -911,7 +911,7 @@
911
911
  {
912
912
  "key": "compaction.handoffArtifactEnabled",
913
913
  "label": "Write pre-compaction handoff artifact",
914
- "description": "Before compaction, write a JSON handoff artifact under ~/.pi/agent/vstack/sessions/<session>/pi-qol/handoff/ containing the previous summary, last task state, and recently-referenced file/artifact paths. Always writes a stamped file and a latest.json pointer. Runs whenever the QOL bounded compaction handler runs — that is, on every budget-guard-triggered compaction (regardless of compaction.customEnabled), and on manual/idle compactions only when compaction.customEnabled is on. Write failures surface a QOL warning and a handoffArtifactError field in the compaction details.",
914
+ "description": "When enabled, write a JSON handoff artifact under ~/.pi/agent/vstack/sessions/<session>/pi-qol/handoff/ containing the previous summary, last task state, and recently-referenced file/artifact paths. Each write creates a stamped file and a latest.json pointer. Applies to every budget-guard compaction (regardless of compaction.customEnabled), and to manual/idle compactions only when compaction.customEnabled is on. Write failures surface a QOL warning and a handoffArtifactError field in the compaction details.",
915
915
  "type": "boolean",
916
916
  "default": true,
917
917
  "category": "Compaction Budget Guard",
@@ -965,7 +965,7 @@
965
965
  "peerDependencies": {
966
966
  "@earendil-works/pi-agent-core": "*",
967
967
  "@earendil-works/pi-ai": "*",
968
- "@earendil-works/pi-coding-agent": "*",
968
+ "@earendil-works/pi-coding-agent": ">=0.80.4",
969
969
  "@earendil-works/pi-tui": "*"
970
970
  },
971
971
  "files": [