@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 +64 -157
- package/extensions/qol/budget-guard-runtime.ts +161 -47
- package/extensions/qol/budget-guard.ts +4 -3
- package/extensions/qol/compaction.ts +7 -7
- package/extensions/qol/constants.ts +4 -3
- package/extensions/qol/session-rename.ts +6 -3
- package/extensions/qol.ts +45 -11
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# pi-qol
|
|
2
2
|
|
|
3
3
|

|
|
4
|
-
|
|
5
4
|

|
|
6
5
|

|
|
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
|
-
| `/
|
|
53
|
-
| `/
|
|
54
|
-
| `/search
|
|
55
|
-
| `/
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
37
|
-
private
|
|
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():
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
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
|
|
72
|
+
/** Returns true when no compaction call is currently active. Visible for tests. */
|
|
45
73
|
get canFire(): boolean {
|
|
46
|
-
return
|
|
74
|
+
return this.inFlight === undefined;
|
|
47
75
|
}
|
|
48
76
|
|
|
49
77
|
get currentKey(): string | undefined {
|
|
50
|
-
return this.
|
|
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
|
-
|
|
61
|
-
|
|
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.
|
|
91
|
+
this.pendingTrigger = undefined;
|
|
92
|
+
this.satisfiedTrigger = undefined;
|
|
66
93
|
return { kind: "no-trigger" };
|
|
67
94
|
}
|
|
68
|
-
if (
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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
|
|
260
|
-
//
|
|
261
|
-
//
|
|
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
|
|
376
|
-
* percent of the model window or an absolute token limit. Returns a stable
|
|
377
|
-
* per crossing so the caller can
|
|
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.
|
|
43
|
-
// absolute token threshold
|
|
44
|
-
// threshold crossing so it does not
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
749
|
-
//
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
|
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": "
|
|
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": [
|