pi-tuck 1.0.19 → 1.0.20

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/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  All notable changes to pi-tuck. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [1.0.20] - 2026-11-09
7
+
8
+ ### Fixed
9
+
10
+ - **pi-tuck cut earlier than native Pi when a model's reply cap is above Pi's reserve.** It reserved the larger of Pi's `compaction.reserveTokens`
11
+ and the model's `maxTokens`. With `maxTokens` 32,768 and a reserve of 16,384 on an 81,920 window, its room fell to 49,152 tokens, where native Pi
12
+ compacts at 65,536. pi-tuck now reserves Pi's `reserveTokens`, native's point. Once a reply on the current branch did not fit, it holds the larger
13
+ of the two for the rest of that branch: a reply stopped for length, or a request pi-ai classes as a context overflow. A transient error such as a
14
+ 503 keeps native's room. This is read from the branch at each decision, so resume, fork and `/tree` follow the branch, and a new session starts
15
+ at `reserveTokens` again. A host that passes its own reserve keeps today's behaviour.
16
+ - Lowering `reserveTokens` now takes effect even when the model's `maxTokens` is larger, until a reply does not fit.
17
+
18
+ ### Release gates
19
+
20
+ - **Shipped without a live gate, because its replay matches 1.0.19:** 13,946 requests on 29 recordings, 0 differ, 0 lost and 0 dead tasks, the same
21
+ 17 clips. Every recording has the reply cap equal to the reserve, where nothing changes.
22
+ - New stress cells with the reply cap above the reserve, at 81,920 and 16,384, on a provider that clips the reply at the window end and one that
23
+ refuses prompt plus cap over the window, plus one with a transient 503. On Pi 1.2.0 and 1.5.0, 1.0.19 refuses in the four cap cells. This version
24
+ takes one clipped or refused reply in each and recovers, with no lost work and no compaction loop; after the 503 it keeps native's room. The full
25
+ stress test (292 cells) has 3 findings on each Pi, all also found on 1.0.19, and no cell is worse. Tests (676) and CI.
26
+ - Sessions that are open keep the build they started with until they are restarted.
27
+
6
28
  ## [1.0.19] - 2026-11-09
7
29
 
8
30
  ### Added
package/README.md CHANGED
@@ -63,9 +63,9 @@ pi install npm:pi-tuck
63
63
  Then start a new Pi session. That is all. pi-tuck takes over Pi's compaction, both automatic and `/compact`. You change no setting.
64
64
 
65
65
  - Needs Pi 1.2.0 or later (tested on 1.2.0 and 1.4.0), on macOS or Linux. On an older Pi, pi-tuck stays off, Pi's own compaction runs, and pi-tuck says so once. Full features on macOS with Apple silicon; elsewhere, compaction and word recall.
66
- - On macOS with Apple silicon, the first session also downloads a local search model in the background (about 390 MB, once). Until it is ready, search works by keyword. See [Semantic search](#semantic-search).
67
- - Prefer GitHub? `pi install git:github.com/galyavin/pi-tuck@v1.0.18`.
68
- - To pin a version, add it: `pi install npm:pi-tuck@1.0.18`.
66
+ - On macOS with Apple silicon, the first session also downloads a local search model in the background (about 390 MB, once), and says once when it is set up. Until it is ready, search works by keyword. See [Semantic search](#semantic-search).
67
+ - Prefer GitHub? `pi install git:github.com/galyavin/pi-tuck@v1.0.19`.
68
+ - To pin a version, add it: `pi install npm:pi-tuck@1.0.19`.
69
69
 
70
70
  ## Uninstall
71
71
 
@@ -102,10 +102,11 @@ pi-tuck's tools appear only after its first cut. Before that there is nothing ou
102
102
 
103
103
  | Command | Does |
104
104
  |---|---|
105
+ | `/tuck` | Says in words what pi-tuck is doing in this session: the version this session loaded (an open session keeps it until Pi restarts), how many cuts pi-tuck made and the last one's size before and after, whether the cut point adapts or is fixed by `cutAt`, and how search works here. It is for you; the model sees nothing new. |
105
106
  | `/tuck-semantic on\|off\|status` | Turns semantic search on or off for this session, or shows its state. |
106
107
  | `/tuck-incident on\|next\|off\|status` | Records this session's compaction decisions to a file you can attach to a bug report. |
107
108
 
108
- `/compact` works as usual, but pi-tuck does not take custom summary instructions. Pi's own setting `"compaction": { "enabled": false }` turns compaction off.
109
+ `/compact` works as usual. Text after it does not steer the summary, because pi-tuck builds the summary from the session itself: since 1.0.19 it cuts as plain `/compact` does and says the text was not used. Pi's own setting `"compaction": { "enabled": false }` turns compaction off.
109
110
 
110
111
  ## What is proven, and what is not
111
112
 
@@ -1 +1 @@
1
- export declare const BUILD_ID = "3bbefa84bf1600296d12131a405f9feaa8fb1ea26ca1a85f5292b40a0a57333f";
1
+ export declare const BUILD_ID = "27846aedbe111110535eeed5998c572182cd15de0efede0fe4df1571166a838e";
@@ -1 +1 @@
1
- export const BUILD_ID = "3bbefa84bf1600296d12131a405f9feaa8fb1ea26ca1a85f5292b40a0a57333f";
1
+ export const BUILD_ID = "27846aedbe111110535eeed5998c572182cd15de0efede0fe4df1571166a838e";
@@ -39,9 +39,9 @@ export interface CompactionOptions {
39
39
  * which is the room less 2% of it, or this fraction of the room when that is lower; a lower value cuts earlier and leaves less. The exact room
40
40
  * stays the limit of every throw, fault and the complete-context guard. If omitted, the band adapts to the last cut. */
41
41
  cutAt?: number;
42
- /** The response reserve: the input room is the context window less this. By default the room never exceeds the window less the model's
43
- * catalogue maximum output, because a transport may omit the output cap and the reply can then grow to that maximum. A host that knows its
44
- * transport's behavior declares an exact reserve with `responseExact: true`, and the room is then the window less `response` exactly. */
42
+ /** A host's response reserve: without `responseExact: true`, the catalogue maximum output stays its floor; with it, `response` is exact.
43
+ * Standalone uses Pi's reserveTokens until a length stop or a context-overflow refusal on the current branch, then also holds the catalogue maximum.
44
+ * Resume, fork and /tree follow that branch's history; a new session has no inherited fallback. */
45
45
  requestReserve?: (ctx: ExtensionContext) => {
46
46
  response: number;
47
47
  responseExact?: true;
@@ -290,16 +290,18 @@ export async function installCompaction(pi, options = {}) {
290
290
  pi.on("agent_settled", () => { admittedRun = false; pending = undefined; phase = "idle"; forgetFailure(); clips.clear(); promptCut = undefined; });
291
291
  const active = () => options.active?.() ?? true;
292
292
  const settingsFor = () => options.settings ?? SettingsManager.inMemory(pi.getSettings());
293
- // The standalone default reserves the model's catalogue maximum output: a transport may omit the output cap, and then only that
294
- // reserve keeps input plus any reply inside the window.
295
- const reserveFor = (ctx) => Math.max(settingsFor().getCompactionSettings(ctx.model).reserveTokens, ctx.model?.maxTokens ?? 0);
296
- // The one place the response reserve is decided; every consumer takes its room from here. A host that knows how its transport caps
297
- // a reply declares an exact reserve (`responseExact: true`) and is held to it; without that flag the catalogue maximum is the floor.
293
+ // The one reserve decision; every consumer takes its room from limits. Standalone starts at Pi's native cut point. Once a reply did
294
+ // not fit (a length stop, or a refusal pi-ai classes as a context overflow), hold the catalogue maximum too. Other errors, such as a 503,
295
+ // keep native's room. Read the whole branch, including history before compactions, each time: no flag to persist or reset on resume,
296
+ // fork, /tree or a new session.
298
297
  const requestReserve = (ctx) => {
299
- const budget = options.requestReserve?.(ctx) ?? { response: reserveFor(ctx) };
298
+ const host = options.requestReserve?.(ctx);
299
+ const budget = host ?? { response: settingsFor().getCompactionSettings(ctx.model).reserveTokens };
300
300
  if (!Number.isSafeInteger(budget.response) || budget.response < 0)
301
301
  throw new Error("Invalid host request reserve");
302
- return { response: budget.responseExact === true ? budget.response : Math.max(budget.response, ctx.model?.maxTokens ?? 0) };
302
+ const floor = host ? host.responseExact !== true : ctx.sessionManager.getBranch().some(entry => entry.type === "message" && entry.message.role === "assistant" &&
303
+ (entry.message.stopReason === "length" || (entry.message.stopReason === "error" && isContextOverflow(entry.message))));
304
+ return { response: floor ? Math.max(budget.response, ctx.model?.maxTokens ?? 0) : budget.response };
303
305
  };
304
306
  const inputRoom = (response, ctx) => {
305
307
  const configured = options.activeInputTokens;
@@ -1,6 +1,6 @@
1
1
  import { type RecallPolicy } from './recall.js';
2
2
  import type { SessionEntry } from '@earendil-works/pi-coding-agent';
3
- export declare const DIAGNOSTIC_BUILD = "3bbefa84bf1600296d12131a405f9feaa8fb1ea26ca1a85f5292b40a0a57333f";
3
+ export declare const DIAGNOSTIC_BUILD = "27846aedbe111110535eeed5998c572182cd15de0efede0fe4df1571166a838e";
4
4
  export interface Receipt {
5
5
  id: string;
6
6
  build: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tuck",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "description": "Deterministic compaction for Pi with no model call, plus exact recall of earlier history. Search by meaning on Apple silicon.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/build-id.ts CHANGED
@@ -1 +1 @@
1
- export const BUILD_ID = "3bbefa84bf1600296d12131a405f9feaa8fb1ea26ca1a85f5292b40a0a57333f";
1
+ export const BUILD_ID = "27846aedbe111110535eeed5998c572182cd15de0efede0fe4df1571166a838e";
package/src/compaction.ts CHANGED
@@ -109,9 +109,9 @@ export interface CompactionOptions {
109
109
  * which is the room less 2% of it, or this fraction of the room when that is lower; a lower value cuts earlier and leaves less. The exact room
110
110
  * stays the limit of every throw, fault and the complete-context guard. If omitted, the band adapts to the last cut. */
111
111
  cutAt?: number;
112
- /** The response reserve: the input room is the context window less this. By default the room never exceeds the window less the model's
113
- * catalogue maximum output, because a transport may omit the output cap and the reply can then grow to that maximum. A host that knows its
114
- * transport's behavior declares an exact reserve with `responseExact: true`, and the room is then the window less `response` exactly. */
112
+ /** A host's response reserve: without `responseExact: true`, the catalogue maximum output stays its floor; with it, `response` is exact.
113
+ * Standalone uses Pi's reserveTokens until a length stop or a context-overflow refusal on the current branch, then also holds the catalogue maximum.
114
+ * Resume, fork and /tree follow that branch's history; a new session has no inherited fallback. */
115
115
  requestReserve?: (ctx: ExtensionContext) => { response: number; responseExact?: true };
116
116
  /** The exact messages the host's `context` hook adds to every final request beyond the saved conversation (for example a role
117
117
  * view), in the form the hook injects them. Cut triggers, the cut planner and the post-cut checks count them with the same
@@ -266,17 +266,19 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
266
266
  pi.on("agent_settled", () => { admittedRun = false; pending = undefined; phase = "idle"; forgetFailure(); clips.clear(); promptCut = undefined; });
267
267
  const active = () => options.active?.() ?? true;
268
268
  const settingsFor = () => options.settings ?? SettingsManager.inMemory(pi.getSettings());
269
- // The standalone default reserves the model's catalogue maximum output: a transport may omit the output cap, and then only that
270
- // reserve keeps input plus any reply inside the window.
271
- const reserveFor = (ctx: ExtensionContext) => Math.max(
272
- settingsFor().getCompactionSettings(ctx.model).reserveTokens, ctx.model?.maxTokens ?? 0);
273
- // The one place the response reserve is decided; every consumer takes its room from here. A host that knows how its transport caps
274
- // a reply declares an exact reserve (`responseExact: true`) and is held to it; without that flag the catalogue maximum is the floor.
269
+ // The one reserve decision; every consumer takes its room from limits. Standalone starts at Pi's native cut point. Once a reply did
270
+ // not fit (a length stop, or a refusal pi-ai classes as a context overflow), hold the catalogue maximum too. Other errors, such as a 503,
271
+ // keep native's room. Read the whole branch, including history before compactions, each time: no flag to persist or reset on resume,
272
+ // fork, /tree or a new session.
275
273
  const requestReserve = (ctx: ExtensionContext) => {
276
- const budget: { response: number; responseExact?: true } = options.requestReserve?.(ctx) ?? { response: reserveFor(ctx) };
274
+ const host = options.requestReserve?.(ctx);
275
+ const budget = host ?? { response: settingsFor().getCompactionSettings(ctx.model).reserveTokens };
277
276
  if (!Number.isSafeInteger(budget.response) || budget.response < 0)
278
277
  throw new Error("Invalid host request reserve");
279
- return { response: budget.responseExact === true ? budget.response : Math.max(budget.response, ctx.model?.maxTokens ?? 0) };
278
+ const floor = host ? host.responseExact !== true : ctx.sessionManager.getBranch().some(entry =>
279
+ entry.type === "message" && entry.message.role === "assistant" &&
280
+ (entry.message.stopReason === "length" || (entry.message.stopReason === "error" && isContextOverflow(entry.message))));
281
+ return { response: floor ? Math.max(budget.response, ctx.model?.maxTokens ?? 0) : budget.response };
280
282
  };
281
283
  const inputRoom = (response: number, ctx: ExtensionContext) => {
282
284
  const configured = options.activeInputTokens;