pi-tuck 1.0.17 → 1.0.19

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +24 -7
  3. package/dist/index.js +7 -2
  4. package/dist/src/budget.d.ts +14 -17
  5. package/dist/src/budget.js +22 -29
  6. package/dist/src/build-id.d.ts +1 -1
  7. package/dist/src/build-id.js +1 -1
  8. package/dist/src/compaction.js +118 -40
  9. package/dist/src/diagnostics.d.ts +1 -1
  10. package/dist/src/history-tools.d.ts +16 -0
  11. package/dist/src/history-tools.js +59 -0
  12. package/dist/src/ledger.d.ts +0 -2
  13. package/dist/src/ledger.js +0 -2
  14. package/dist/src/omissions.d.ts +1 -1
  15. package/dist/src/omissions.js +88 -6
  16. package/dist/src/status.d.ts +19 -0
  17. package/dist/src/status.js +36 -0
  18. package/dist/src/tuck/core/brief.d.ts +6 -3
  19. package/dist/src/tuck/core/brief.js +27 -9
  20. package/dist/src/tuck/core/filter-noise.d.ts +1 -1
  21. package/dist/src/tuck/core/filter-noise.js +1 -3
  22. package/dist/src/tuck/core/normalize.d.ts +2 -5
  23. package/dist/src/tuck/core/normalize.js +15 -33
  24. package/dist/src/tuck/core/shrink.d.ts +6 -14
  25. package/dist/src/tuck/core/shrink.js +162 -102
  26. package/dist/src/tuck/core/summarize.d.ts +3 -14
  27. package/dist/src/tuck/core/summarize.js +9 -20
  28. package/dist/src/tuck/core/transcript.d.ts +5 -0
  29. package/dist/src/tuck/core/transcript.js +27 -4
  30. package/dist/src/tuck/types.d.ts +1 -1
  31. package/dist/src/tuck.d.ts +9 -17
  32. package/dist/src/tuck.js +384 -212
  33. package/index.ts +4 -2
  34. package/package.json +1 -1
  35. package/src/budget.ts +24 -26
  36. package/src/build-id.ts +1 -1
  37. package/src/compaction.ts +107 -38
  38. package/src/history-tools.ts +57 -0
  39. package/src/ledger.ts +0 -2
  40. package/src/omissions.ts +64 -7
  41. package/src/status.ts +43 -0
  42. package/src/tuck/core/brief.ts +25 -10
  43. package/src/tuck/core/filter-noise.ts +1 -3
  44. package/src/tuck/core/normalize.ts +11 -27
  45. package/src/tuck/core/shrink.ts +141 -91
  46. package/src/tuck/core/summarize.ts +9 -24
  47. package/src/tuck/core/transcript.ts +26 -4
  48. package/src/tuck/types.ts +3 -3
  49. package/src/tuck.ts +336 -185
package/CHANGELOG.md CHANGED
@@ -3,6 +3,78 @@
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.19] - 2026-11-09
7
+
8
+ ### Added
9
+
10
+ - **`/tuck` says in words what pi-tuck is doing in this session:** the version the session loaded (an open session keeps it until Pi restarts),
11
+ 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
12
+ here. It is a command for the operator; the model sees nothing new.
13
+ - **The first-time semantic setup says when it is done.** It announced its download and then said nothing; now it says once that semantic
14
+ recall is set up.
15
+
16
+ ### Changed
17
+
18
+ - **`/compact` with text after it now cuts.** pi-tuck builds its summary from the session itself, so the text cannot steer it. Before, the cut was
19
+ cancelled, which Pi showed as an error; now pi-tuck makes the cut plain `/compact` makes and says the text was not used.
20
+
21
+ ### Release gates
22
+
23
+ - **Shipped without a live gate, because its replay matches 1.0.18:** 13,946 requests on 29 recordings, 0 differ, 0 lost and 0 dead tasks, the same
24
+ 17 clips. It changes nothing the model sees: `/tuck`, the setup notice and the `/compact` notice are for the operator only. Tests (662), CI, and
25
+ the settings stress test (287 cells, none worse than 1.0.18, no cut over its target).
26
+ - Sessions that are open keep the build they started with until they are restarted.
27
+
28
+ ## [1.0.18] - 2026-11-08
29
+
30
+ ### Fixed
31
+
32
+ - **A summary could drop the task a session was working on (1.0.13 to 1.0.17).** Separate rules decided what a summary quoted and what the kept tail
33
+ held. In a session with many turns, the prompt that set the current task, or the first prompt, could lose its opening after a cut. The settings stress
34
+ test found it 40 times in 32 of its cells on 1.0.16 and 1.0.17. A cut now fills its room by one ranked list, across the summary and the kept tail
35
+ together:
36
+ 1. the current prompt, whole;
37
+ 2. the openings of the first prompt and the newest answered prompts, sized to the room;
38
+ 3. the newest part of the conversation, kept as it is;
39
+ 4. the file and action lists;
40
+ 5. the rest of the conversation.
41
+
42
+ The stress test now finds no lost opening in 287 cells. Dropped turns and tool results are counted with index ranges you can read back with
43
+ `tuck_recall`.
44
+
45
+ ### Changed
46
+
47
+ - **One number sets how much a cut carries, and the decision to cut reads only amounts.** The quote budget and its fallback steps are gone, and so are
48
+ the rules that compared two versions of a summary. The kept tail is at most half of the 25,000-token tail cap, the size the cut point already assumed.
49
+ - **pi-tuck's tools appear only after its first cut.** Before that, there is nothing out of view to recall. Until then a request is byte-identical to one
50
+ without pi-tuck: 254 of 254 requests on replay. Every cut plans room for the tools the requests after it carry.
51
+ - **A prompt's late cut is planned when the prompt joins the session,** after other extensions have changed the system prompt and before Pi fixes the
52
+ request's tools. It replaces 1.0.17's reduction in the final guard and keeps its effect: a host section added in any load order is cut, not refused.
53
+ - **Cuts compute faster:** about half the time of 1.0.17 on the regression sessions, and 76 ms against 159 ms at 3,888 entries.
54
+ - An answered prompt with an image keeps its text opening, and a current prompt of only spaces is kept.
55
+
56
+ ### Correction to 1.0.16
57
+
58
+ - 1.0.16's notes said its cut rate and cost were unchanged. That held on replay of single-task sessions, but not in live 30-task chains at 81,920
59
+ against 1.0.13. There it took 1.48 times the turns (95% interval 1.34 to 1.64), 74% more cuts and 1.66 times the tokens, with resolved 56 against 55
60
+ ([benchmark/RESULTS-V1016.md](benchmark/RESULTS-V1016.md)). Most of the extra cuts follow from the extra turns. Replay cannot show this cost:
61
+ replay puts 1.0.13 and 1.0.17 within 0.1% of each other.
62
+
63
+ ### Known issues
64
+
65
+ - On replay of 17 recordings, estimated request time against 1.0.13 is 2.2% higher quiet and 4.2% loaded at 81,920, and 1.1% and 1.9% lower at
66
+ 131,072. Billed input moves the same way. At 81,920 each cut carries about 600 more tokens.
67
+ - Two stress findings from earlier versions remain: a cut that frees little with a 40,000-token setup at 65,536, and a fresh assistant reply larger than
68
+ the free room.
69
+ - Sessions that are open keep the build they started with until they are restarted.
70
+
71
+ ### Release gates
72
+
73
+ - **Shipped before its live gate, as a fix for lost work.** The gate runs right after release; if it fails, npm and this version are rolled back to
74
+ 1.0.17.
75
+ - Replay on 29 recordings: 0 lost tasks, 0 dead tasks, 0 faults, every clip read back.
76
+ - Tests, CI, the settings stress test (287 cells, no cut over its target), and the shape checks (21 cells at 32,768, 81,920 and 131,072).
77
+
6
78
  ## [1.0.17] - 2026-11-08
7
79
 
8
80
  ### Fixed
package/README.md CHANGED
@@ -64,17 +64,30 @@ Then start a new Pi session. That is all. pi-tuck takes over Pi's compaction, bo
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
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.16`.
68
- - To pin a version, add it: `pi install npm:pi-tuck@1.0.16`.
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`.
69
+
70
+ ## Uninstall
71
+
72
+ ```sh
73
+ pi remove npm:pi-tuck
74
+ ```
75
+
76
+ Use the same source you installed with. This takes pi-tuck out of Pi's settings. It does not delete what the search setup downloaded: about 460 MB in the `pi-tuck` folder of Pi's agent folder (`~/.pi/agent/pi-tuck`, or the folder `PI_CODING_AGENT_DIR` names). To free the space, delete that folder:
77
+
78
+ ```sh
79
+ rm -rf ~/.pi/agent/pi-tuck
80
+ ```
81
+
82
+ On Linux and Intel Macs, nothing is downloaded, so there is nothing to delete.
69
83
 
70
84
  ## How it works
71
85
 
72
86
  When context fills up, pi-tuck replaces the older turns with one summary. The summary keeps:
73
87
 
74
- - **The dialogue.** What you asked and what the agent answered, in order, word for word. If it is too long for the room, it shrinks: the oldest assistant text is clipped first, then older prompts. Clipped text says how to read it in full.
88
+ - **The dialogue and your prompts, ranked.** Since 1.0.18, one ranked list decides what a cut carries, across the summary and the kept tail together: the current prompt, whole; the openings of the first prompt and the newest answered prompts, sized to the room; the newest part of the conversation, kept as it is; the file and action lists below; and the rest of the conversation. Dropped turns and tool results are counted with index ranges that `tuck_recall` reads back. Clipped text says how to read it in full.
75
89
  - **The files.** Files the agent read, wrote or edited, newest first (up to 60), each with a pointer to read it back.
76
90
  - **The action trail.** The agent's other tool calls, such as shell commands, newest first (up to 30). The output is left out. A pointer leads to it.
77
- - **Your prompts, quoted from both ends** (1.0.16 and later): first the opening of every recent answered turn, then the first prompt's opening (up to 2,000 characters), then more of the newest turns, then the rest of the first prompt. A turn that does not fit keeps its opening and a locator to the rest. The budget is 15% of the room and never less than 12,000 characters. When room is tight, the quotes shrink step by step: the extensions first, then the openings, then the first prompt alone. Before 1.0.16 a summary quoted only the first prompt (up to 12,000 characters) and the newest turn; see Known limits.
78
91
 
79
92
  Tool output stays in the session file, not in the summary. Each summary ends with a short instruction to the agent: re-read a file before you edit it, look things up, and treat anything not shown as unknown.
80
93
 
@@ -85,7 +98,7 @@ The agent gets two tools to get anything back:
85
98
  - **`tuck_recall`** finds earlier text by keyword. It reads any original message or tool result exactly, by the index shown in a summary. It can also write an original out to a file.
86
99
  - **`tuck_search`** (macOS with Apple silicon only) finds earlier history related to a question, even when the wording differs. It combines keyword hits with local semantic search and returns the original text with pointers for `tuck_recall`.
87
100
 
88
- On Apple silicon, both tools cost about 1,050 tokens of tool description per request. Elsewhere only `tuck_recall` is offered.
101
+ pi-tuck's tools appear only after its first cut. Before that there is nothing out of view to recall, and a request is byte-identical to one without pi-tuck (254 of 254 requests on replay). After the first cut, on Apple silicon, both tools cost about 1,050 tokens of tool description per request. Elsewhere only `tuck_recall` is offered.
89
102
 
90
103
  | Command | Does |
91
104
  |---|---|
@@ -114,8 +127,10 @@ Known limits today:
114
127
  - Small windows: an upstream that spends far more context on tool schemas than others (some OpenRouter upstreams do) can leave a small input room too little space. pi-tuck then refuses to cut rather than overflow. This made 1.0.1 refuse at a 32k window with Pi's default 16,384 reserve. Version 1.0.4 added a notice that says when the provider counts far above pi-tuck's estimate. In 1.0.4, after an oversize tool result or a "Nothing to compact" error from Pi, a session could keep refusing later prompts; 1.0.5 fixed both. Version 1.0.8 clips a fresh tool result that is too large for the room (its head and tail, with a `tuck_recall` locator to the rest) instead of refusing the run. If a small window still refuses, pin a different upstream, lower the reserve, or use a larger window; see [Small windows](#small-windows).
115
128
  - Small window, after a stopped run (1.0.8 and 1.0.9 only; fixed in 1.0.10): Pi's default `compaction.keepRecentTokens` (20,000) can exceed pi-tuck's input room, which is the window less the larger of `compaction.reserveTokens` and the model's maximum output (16,384 at a 32k window with the default reserve). After a run that is stopped (Esc, or a time limit) with the context above the room, every later prompt could be refused with "The request needs a cut and Pi has none to make (Nothing to compact (session too small))", and the session stayed stuck. In one check at these defaults (pi-tuck 1.0.8, 32k window, one session of 30 tasks, one local model), 28 tasks were lost, tasks 3 to 30 ([`benchmark/RESULTS-DEFAULTS.md`](benchmark/RESULTS-DEFAULTS.md)). If you are on 1.0.8 or 1.0.9, upgrade to 1.0.10. In 1.0.10 the cut a prompt needs is pi-tuck's own, so `keepRecentTokens` no longer leaves it with nothing to cut. A rerun of the same check on the 1.0.10 candidate had no refusal and no stuck session. It had two caveats: two of its tasks had a first request the local server never answered, and 8 of its 30 tasks began without their prompt, which the refusal counter cannot see (fixed in 1.0.12; see Benchmarks and [`benchmark/RESULTS-DEFAULTS-1010.md`](benchmark/RESULTS-DEFAULTS-1010.md)). Windows of 128k and up were not affected at the defaults.
116
129
  - A prompt could be lost, with no notice (1.0.10 and 1.0.11; fixed in 1.0.12). When a new prompt arrived over the cut point and pi-tuck's cut kept nothing of the conversation before it, which happens when everything since the last cut is one long turn, the prompt was dropped from the request and from the saved session. The model never saw it and usually went on with the previous task, so work you asked for could be lost, and could look answered. The next prompt ran normally. If you used 1.0.10 or 1.0.11 and a reply ignored what you had just asked, this is the likely cause. Upgrade to 1.0.12 ([`CHANGELOG.md`](CHANGELOG.md) 1.0.12).
117
- - A long session could lose the task it was working on (1.0.0 to 1.0.15; fixed in 1.0.16). A summary quoted only the session's first user turn, up to 12,000 characters, and the newest one. Every other user turn competed in the shrunk dialogue, which in a long session clips user turns to a few words and drops the oldest. In a session that runs task after task, the current task is an older turn followed by short steps, so it was clipped or dropped while a finished first task stayed whole, and agents acted on a days-old task. Short sessions, and the SWE-bench chains behind our benchmarks, keep each task's prompt in the kept tail and never hit it. Upgrade to 1.0.16 ([`CHANGELOG.md`](CHANGELOG.md) 1.0.16).
118
- - At very small free room (a few thousand tokens or less), the quotes give way and an older prompt can still lose its opening, in 1.0.16: 40 findings in 32 stress cells, against 447 in 168 cells on 1.0.15 ([`CHANGELOG.md`](CHANGELOG.md) 1.0.16).
130
+ - A host that patches a large section into the system prompt could get a refusal that a cut would have fit (1.0.0 to 1.0.16; fixed in 1.0.17). pi-tuck plans a prompt's cut when the prompt arrives, before other extensions change the system prompt. A host engine that adds a section of thousands of tokens for one turn could push that turn's first request over the room, and the final guard refused it. Now the guard plans the cut against the system prompt as finally sent, and refuses only if that cut does not fit either ([`CHANGELOG.md`](CHANGELOG.md) 1.0.17).
131
+ - A summary could drop the task a session was working on (1.0.0 to 1.0.17; fixed in 1.0.18). Until 1.0.15 a summary quoted only the session's first user turn, up to 12,000 characters, and the newest one, so in a long session the current task, an older turn followed by short steps, was clipped or dropped while a finished first task stayed whole, and agents acted on a days-old task. 1.0.16 added more quotes. From 1.0.13 to 1.0.17, separate rules decided what a summary quoted and what the kept tail held, and the prompt that set the current task, or the first prompt, could lose its opening after a cut: the stress test found it 40 times in 32 cells on 1.0.16 and 1.0.17. Since 1.0.18 one ranked list fills a cut's room, and the stress test finds no lost opening in 287 cells. Short sessions, and the SWE-bench chains behind our benchmarks, keep each task's prompt in the kept tail and never hit it. Upgrade to 1.0.18 ([`CHANGELOG.md`](CHANGELOG.md) 1.0.18).
132
+ - pi-tuck 1.0.16 cost more in live chains than its notes first said. Against 1.0.13 at an 81,920 window, it took 1.48 times the turns on the tasks both resolved (95% interval 1.34 to 1.64), 74% more cuts and 1.66 times the tokens, with 56 of 60 tasks resolved against 55. Replay of single-task sessions cannot show this cost. 1.0.18 replaced its quote rules; in the 1.0.18 gate it took 22.27 turns per task against 29.35 for 1.0.16 and 20.22 for the 1.0.13 arm (different pairs of runs; reported, not claimed) ([`benchmark/RESULTS-V1016.md`](benchmark/RESULTS-V1016.md), [`CHANGELOG.md`](CHANGELOG.md) 1.0.18).
133
+ - Since 1.0.18, on replay of 17 recordings, estimated request time against 1.0.13 is 2.2% higher quiet and 4.2% higher loaded at 81,920, and 1.1% and 1.9% lower at 131,072. Billed input moves the same way. At 81,920 each cut carries about 600 more tokens. Sessions that are open keep the build they started with until they are restarted ([`CHANGELOG.md`](CHANGELOG.md) 1.0.18).
119
134
  - The cut point adapts by itself since 1.0.13; before it, pi-tuck cut too often at small windows or with a large setup (1.0.11 and 1.0.12). The trade, from the changelog: on windows that cut too often, each request now carries more context. Cuts, re-processing and request time fall, and billed tokens rise on paid models. On replay of real sessions at an 81,920 window, cuts went from 22.3 to 9.3 per 100 requests, request time was about 16 to 20% lower, and billed tokens were 16 to 19% higher; at a 65,536 window billed tokens rose by as much as 35%. A very large setup at a small window, such as 40k at 65,536, still leaves too little free room for a good rhythm: cuts stay frequent and free little ([`CHANGELOG.md`](CHANGELOG.md) 1.0.13).
120
135
  - Still applying (1.0.11 to 1.0.16): a fresh assistant message larger than the free room is refused, and a short next prompt recovers (in an unattended `pi -p` run, a refusal ends the run). A reply that reports more than the window reads to Pi as a silent overflow; when nothing can be cut, pi-tuck refuses the next prompt, and the prompt after that runs. If a provider keeps rejecting requests in a wording Pi does not class as a context overflow, or with a bare HTTP 413, pi-tuck does not recover on its own: the session stays intact, and every prompt can fail until the request shrinks. Try `/compact` ([`CHANGELOG.md`](CHANGELOG.md) 1.0.13).
121
136
  - Other extensions (1.0.10): a prompt's cut no longer goes through Pi's `session_before_compact`, so another extension's veto there no longer stops it. It never stopped a cut between tool rounds. Pi's own threshold and overflow cuts, and `/compact`, still ask other extensions as before.
@@ -130,6 +145,8 @@ Each row says what was measured, under which conditions, and where the data is.
130
145
  | Pi's default compaction settings, 32k window, pi-tuck 1.0.10 candidate | Passed as registered: 0 dead tasks and 0 stuck sessions on 30 tasks, on a slow local server (27 of 30 tasks hit the 30-minute wall cap, and 2 had a first request the server never answered). 6 resolved. A counter written later, which counts a task with no token as dead, would count those 2. **The registered counter counts refusals and cannot see a task that began without its prompt**: in 8 of the 30 tasks (4, 8, 9, 10, 13, 14, 17, 21) a cut at the prompt kept no turn, none of the 8 resolved, and 6 ended with an empty patch. So the pass shows no refusal, not that every task began with its prompt. The 1.0.11 candidate on the same test had 7 of 30 such tasks, 1 resolved. Fixed in 1.0.12 | Candidate `a67ec30` of 1.0.10, `mac-studio/main`, window 32,768, output cap 16,384, Pi's default `reserveTokens` 16,384 and `keepRecentTokens` 20,000, one session of chain t06. One mid-run refusal (a round with 9,020 tokens of reasoning against the 16,384-token room). One session per arm, one model. In 12 of the 15 tasks across both arms the model's own first messages show it lacked the prompt; the other 3 are not clear from the records | [`benchmark/RESULTS-DEFAULTS-1010.md`](benchmark/RESULTS-DEFAULTS-1010.md) |
131
146
  | Pi's default compaction settings, 32k window, pi-tuck 1.0.8 | 28 of 30 tasks lost (tasks 3 to 30, each refused at its prompt); 1 resolved. Fails the pre-registered bar of 0 lost tasks | pi-tuck 1.0.8 (`86935e9`), `mac-studio/main`, window 32,768, output cap 16,384, `reserveTokens` 16,384 and `keepRecentTokens` 20,000 (larger than the room), one session of chain t06. One session, one model, one build: it shows that the failure occurs at these settings, not how often. Not comparable with other rows | [`benchmark/RESULTS-DEFAULTS.md`](benchmark/RESULTS-DEFAULTS.md) |
132
147
  | Tasks resolved, 32k window | Pi's built-in compaction 94 of 150; pi-tuck 120 of 150 (+17.3 points, 95% lower bound +10.7); observation masking 117 of 150 (pi-tuck +2.0, lower bound −2.7: no difference shown). Turns per task: 9.4 (built-in), 24.0 (pi-tuck), 11.9 (masking). Tokens per session: 6.10 M, 6.39 M, 7.02 M. Spend per arm for the five sessions: $0.87, $0.55, $0.94 | pi-tuck 1.0.5 (commit `aa30824`, the 1.0.5 code; 1.0.6 and 1.0.7 are not measured). DeepSeek V4.1 Flash pinned to one upstream, window 32,768, reserve 16,384, 5 chains of 30 SWE-bench Verified tasks, paired by task, one-sided bootstrap over tasks. **Limits:** only pi-tuck is held to the 16,384-token room (the built-in sent a request over it in 97 of 150 tasks, masking in 73, so the window is nominal for them). The difference by chain is +10.0, −3.3, +50.0, +16.7 and +13.3 points; leaving out chain t03 it is +9.2. The bootstrap resamples tasks, not chains. The pi-tuck arm ran at a different time from the others. One model, one window. pi-tuck refused to cut mid-run in 5 tasks, where one fresh tool result was larger than the room; they count as unresolved. In the same setup pi-tuck 1.0.3 resolved 72 of 150, with 52 dead tasks (tasks refused at their prompt after an earlier failure). In the pi-tuck arm of this row there was 1 dead task (t03 task 24: refused at its prompt with "Nothing to compact"; the next task ran normally), so this build missed the pre-registered acceptance bar of zero dead tasks by one task (the other part of the bar, no loss against 1.0.3 by more than 3 points, was met) | [`benchmark/RESULTS-32K-ACCEPT.md`](benchmark/RESULTS-32K-ACCEPT.md) |
148
+ | pi-tuck 1.0.18 against the recorded 1.0.16 arm, 81,920 window | Registered bar (Tier 1) met: both arms resolved the same 56 of 60 tasks (+0.0 points, one-sided 95% lower bound +0.0, against a bar of above −10); 0 lost tasks: dead tasks, mid-run refusals and dropped prompts are 0 in both arms, and the audit listed one candidate task (t06 task 21), which, with the two other candidate tasks that had a cut at a prompt (t06 task 8, t07 task 24), was read by hand and works on its own task. Readouts: turns per task 22.27 against 29.35, cuts 57 against 61, total tokens 40.3 M against 62.1 M. Against the recorded 1.0.13 arm of the adaptive gate, a different pair of runs a day apart, 1.0.18 is at 22.27 turns per task against 20.22 and 40.3 M tokens against 37.4 M | Pre-registered gate. pi-tuck 1.0.18 (`2d1f830`, tag `v1.0.18`), run from the unpacked npm tarball; its `src/` is byte-identical to `1ea1e1e`, the commit replay and the stress test measured, ran alone on chains t06 and t07 (60 tasks) against the recorded 1.0.16 arm; the arms did not run side by side, the base earlier the same day, so seconds are not compared. One model, one window, one run per arm. The resolved result is a bound, not a gain. The turn, cut and token readouts are reported, not claimed | [`benchmark/RESULTS-V1018.md`](benchmark/RESULTS-V1018.md) |
149
+ | pi-tuck 1.0.16 against the recorded 1.0.13 arm, 81,920 window | Registered bar (Tier 1) met: resolved 56 of 60 against 55 (+1.7 points, one-sided 95% lower bound +0.0, against a bar of above −10); 0 lost tasks. Readouts: turns per task 29.35 against 20.22 (1.48 times on the 55 tasks both resolved, 95% interval 1.34 to 1.64), cuts 61 against 35, total tokens 62.1 M against 37.4 M | Pre-registered gate. pi-tuck 1.0.16 (`dee0eab`) ran alone on chains t06 and t07 (60 tasks) against the recorded 1.0.13 arm from the adaptive gate; the two arms did not run side by side, the base a day earlier, so seconds are not compared. One model, one window, one run per arm. The resolved result is a bound, not a gain. The cost readouts are reported, not claimed. 1.0.18 replaced this build's quote rules | [`benchmark/RESULTS-V1016.md`](benchmark/RESULTS-V1016.md) |
133
150
  | The adaptive cut point (1.0.13) against 1.0.12, 81,920 window | Registered bar (Tier 2) met: resolved 131 of 150 in both arms (+0.0 points, one-sided 95% lower bound −1.3, against a bar of above −5); 0 lost tasks in the candidate arm. The 0 counts dead tasks, tasks whose prompt was dropped at a cut, and tasks an audit found started without their prompt; all 11 tasks with a cut at a prompt, in both arms, were also read by hand, and in each the model's first messages work on that task | Pre-registered gate. pi-tuck 1.0.13 (`6f13685`) is the exact code the gate measured (`882f6f6`); only the version and the notes changed. It was measured against pi-tuck 1.0.12, `mac-studio/main` at xhigh, window 81,920, reserve 16,384, `keepRecentTokens` 20,000, 5 chains (t06 to t10) of 30 tasks, 150 paired tasks, one run per arm, one model throughout. The resolved result is a bound, not a gain. Turns, tokens and seconds are in the file and are not claimed here. Nothing about other windows, models or builds | [`benchmark/RESULTS-ADAPTIVE.md`](benchmark/RESULTS-ADAPTIVE.md) |
134
151
  | The 1.0.10 candidate against 1.0.9, 32k window | 0 dead tasks in both arms; no mid-run refusal. Resolved 50 of 60 (1.0.9) and 49 of 60 (1.0.10 candidate): −1.7 points, 95% lower bound −8.3. **The gate failed as registered, by one task**; 1.0.10 shipped as an exception because of the small-window defect. 6 of the 1.0.10 candidate's 60 tasks began without their prompt (4 of them resolved), against 0 in the 1.0.9 arm; fixed in 1.0.12. Turns per task 25.08 and 29.12; cuts 731 and 884; total tokens 13.07 M and 15.12 M | Pre-registered gate. pi-tuck `a67ec30` against 1.0.9 (`cd4626b`). DeepSeek V4.1 Flash pinned to one upstream, window 32,768, reserve 16,384, `keepRecentTokens` 3,333, chains t01 and t02, 60 paired tasks. One model, one window, one run per arm; 60 tasks cannot separate a one-task difference from run-to-run variation | [`benchmark/RESULTS-V110.md`](benchmark/RESULTS-V110.md) |
135
152
  | The 1.0.8 candidate against 1.0.7, 32k window | 0 dead tasks in both arms. Resolved 46 of 60 in both (+0.0 points, 95% lower bound −6.7). No mid-run refusal in either arm. Turns per task 25.37 (1.0.7) and 26.10 (1.0.8) | Pre-registered gate, passed as registered. pi-tuck `a2e7d1a` (the tool-result clip and the cut at the end of a run, on 1.0.6) against 1.0.7. Released 1.0.8 is that build merged with 1.0.7's Metal-only recall; its replay requests are reported byte-identical to the gated build on 29 recordings (stated in the source file, not re-run for this README). DeepSeek V4.1 Flash pinned to one upstream, window 32,768, reserve 16,384, chains t01 and t02, 60 paired tasks, both arms run together. One model, one window, one run per arm; 60 tasks cannot show a difference smaller than the bound. The run shows that the change introduced no refusal; no refusal occurred in the 1.0.7 arm for the clip to prevent | [`benchmark/RESULTS-V108.md`](benchmark/RESULTS-V108.md) |
package/dist/index.js CHANGED
@@ -50,7 +50,7 @@ export default async function (pi) {
50
50
  const handle = await installCompaction(pi, { semanticRecall, search: appleSilicon, cutAt: settings.cutAt, onSemanticSetting: async (enabled) => { off = !enabled; } });
51
51
  if (settings.semanticRecall === false)
52
52
  return;
53
- let session = 0, said = "", setup;
53
+ let session = 0, said = "", setup, announced = false;
54
54
  // A context outlives its session (a setup can finish after /new); a notice to an ended session is dropped.
55
55
  const say = (ctx, message, level) => { try {
56
56
  ctx.ui.notify(message, level);
@@ -76,12 +76,17 @@ export default async function (pi) {
76
76
  return; // a one-shot run (pi -p) would end before a first-time setup could
77
77
  if (!semanticRecall.status().configured)
78
78
  semanticRecall.refuse("Semantic recall is being set up; word recall works meanwhile");
79
- setup ??= ensureSemanticRuntime(defaultRoot(), message => say(ctx, message, "info"));
79
+ setup ??= ensureSemanticRuntime(defaultRoot(), message => { announced = true; say(ctx, message, "info"); });
80
80
  setup.then(async () => {
81
81
  if (current !== session || off)
82
82
  return;
83
83
  try {
84
84
  await handle.setSemanticEnabled(true, ctx); // reads the runtime's configuration through the outside-project check
85
+ // The download was announced, so its end is too: once, when semantic recall first starts on it.
86
+ if (announced && current === session) {
87
+ announced = false;
88
+ say(ctx, "pi-tuck: semantic recall is set up. Search by meaning works from the first cut on.", "info");
89
+ }
85
90
  }
86
91
  catch (error) {
87
92
  if (current === session)
@@ -25,11 +25,19 @@ export declare const validCutAt: (value: unknown) => value is number;
25
25
  /** The initial default cut point: 60% of the room before a usable previous cut. Measured against 1 with paired replays (no answer loss) and billed
26
26
  * input under automatic and explicit prompt caching (about 17% less at 128k, 10-14% at 32k). */
27
27
  export declare const DEFAULT_CUT_AT = 0.6;
28
+ /** Preferred native carry, in the planner's estimate: never more than the
29
+ * keep-recent cut offers or the tail amount the adaptive band assumes. */
30
+ export declare const carriedTailTokens: (offeredTokens: number) => number;
28
31
  export declare const bandOf: (room: number, cutAt?: number, adaptive?: {
29
32
  system: number;
30
33
  tokensAfter: number;
31
34
  factor: number;
32
35
  }) => number;
36
+ /** The cut's one carried-context grant, in the planner's estimate. Combines
37
+ * the existing native/fixed base with the quarter-free historical amount;
38
+ * neither component remains an independent spending limit in the fill.
39
+ * The grant depends only on quantities. */
40
+ export declare function carriedTarget(window: number, reserve: number, base: number, before: number): number;
33
41
  /**
34
42
  * Replayed reasoning that Pi's estimator does not see. On the OpenAI Responses APIs an assistant turn's reasoning item rides in its thinking
35
43
  * block's `thinkingSignature` (encrypted) and is sent back with later requests (pi-ai openai-responses-shared; transform-messages skips
@@ -61,11 +69,11 @@ export declare const measurableConversationTokens: (messages: Messages, ctx: Ext
61
69
  /** Raw reserve for the cut planner: the window less the raw tokens that fit the target room, plus the input no cut can remove. */
62
70
  export declare const planningReserve: (window: number, targetRoom: number, factor: number, mandatory: number) => number;
63
71
  export type Target = "band" | "room";
64
- /** A planned cut: the conversation it leaves, and the summary view that produced it (whole dialogue or shrunk). */
72
+ /** A planned cut: the conversation it leaves. The optional mode is diagnostic data, not a decision input. */
65
73
  export type Plan<D extends object = {}> = ({
66
74
  draft: true;
67
75
  after: number;
68
- mode: string;
76
+ mode?: string;
69
77
  } & D) | {
70
78
  draft: false;
71
79
  capacityRejected: boolean;
@@ -110,21 +118,10 @@ export interface CutInput<D extends object = {}> {
110
118
  export type Verdict<D extends object = {}> = Exclude<Decision<D>, {
111
119
  kind: "defer";
112
120
  }>;
113
- /**
114
- * Rule 4. The exact room is planned first. When its plan already lands below the band it is taken as it is (target "room"): it keeps the
115
- * room plan's content exactly, because every allowance of the planner is derived from the reserve and the band's is smaller. Only
116
- * otherwise is the band planned. Inside the band a cut is opportunistic, so a failed plan is never a fault and never falls back to the
117
- * room (a cut that still lands in the band would trigger again every round); the exact room would cut nothing there, so a band cut is
118
- * taken only when the room can plan a cut too and with the same mode (a lossy view the room could not even plan is a quality change with
119
- * no fault to prevent). At or above the room, where a cut is due, a band plan of another mode gives way to the room plan (today's
120
- * view), and a band plan stands when the room has none (a cut instead of a fault).
121
- * A lean band, one the cut point binds (`band < bandOf(room)`), is planned first and the room plan landing below it is not taken for that:
122
- * the room plan keeps a native tail of up to half the room, so it can land just under a band that far below the room, and the next round
123
- * would cut again, freeing little. The room is planned only where the rules above use it: to compare the mode, or when the band has no plan
124
- * and a cut is due. An adaptive band can also reach the margin, in which case the room plan is tried first.
125
- * The margin adds no new decision fault. Exceptions thrown by `plan` are not caught here: they are faults, and they now surface at
126
- * boundaries inside the band too, where no planner ran before the margin.
127
- */
121
+ /** Plan the band first at every cut point. An opportunistic cut must land
122
+ * below it and free the existing minimum. A due cut takes a band draft, or
123
+ * falls back to the room only when the band has no draft. Final reduction and
124
+ * room-fit checks own faults. Planner exceptions propagate. */
128
125
  export declare function chooseCut<D extends object = {}>(input: CutInput<D> & {
129
126
  opportunistic: boolean;
130
127
  }): Verdict<D>;
@@ -14,11 +14,24 @@ export const validCutAt = (value) => typeof value === "number" && value > 0 && v
14
14
  /** The initial default cut point: 60% of the room before a usable previous cut. Measured against 1 with paired replays (no answer loss) and billed
15
15
  * input under automatic and explicit prompt caching (about 17% less at 128k, 10-14% at 32k). */
16
16
  export const DEFAULT_CUT_AT = 0.6;
17
+ /** Preferred native carry, in the planner's estimate: never more than the
18
+ * keep-recent cut offers or the tail amount the adaptive band assumes. */
19
+ export const carriedTailTokens = (offeredTokens) => Math.min(offeredTokens, MAX_TAIL_TOKENS / 2);
17
20
  export const bandOf = (room, cutAt = 1, adaptive) => {
18
21
  const positive = Math.max(0, room);
19
22
  const base = Math.floor(positive * cutAt);
20
- return Math.min(room - Math.ceil(positive * TRIGGER_MARGIN_FRACTION), adaptive ? Math.max(base, Math.ceil((adaptive.system + adaptive.tokensAfter + MAX_TAIL_TOKENS / 2) * adaptive.factor)) : base);
23
+ return Math.min(room - Math.ceil(positive * TRIGGER_MARGIN_FRACTION), adaptive ? Math.max(base, Math.ceil((adaptive.system + adaptive.tokensAfter + carriedTailTokens(MAX_TAIL_TOKENS)) * adaptive.factor)) : base);
21
24
  };
25
+ /** The cut's one carried-context grant, in the planner's estimate. Combines
26
+ * the existing native/fixed base with the quarter-free historical amount;
27
+ * neither component remains an independent spending limit in the fill.
28
+ * The grant depends only on quantities. */
29
+ export function carriedTarget(window, reserve, base, before) {
30
+ const room = Math.max(0, window - reserve - 1);
31
+ const free = Math.max(0, window - reserve - base - 1);
32
+ const historical = Math.max(0, Math.min(Math.floor(free / 4), before - base - 1));
33
+ return Math.min(room, before - 1, base + historical);
34
+ }
22
35
  /** The APIs whose request converter (pi-ai openai-responses-shared) sends each signed thinking block back as an input reasoning item. */
23
36
  const REPLAYING_APIS = new Set(["openai-responses", "azure-openai-responses", "openai-codex-responses"]);
24
37
  /**
@@ -70,45 +83,25 @@ export const projectedConversationTokens = (messages, ctx, policy) => conversati
70
83
  export const measurableConversationTokens = (messages, ctx, policy) => conversationTokens(measurableMessages(messages, ctx, policy));
71
84
  /** Raw reserve for the cut planner: the window less the raw tokens that fit the target room, plus the input no cut can remove. */
72
85
  export const planningReserve = (window, targetRoom, factor, mandatory) => window - Math.floor(targetRoom / factor) + mandatory;
73
- /**
74
- * Rule 4. The exact room is planned first. When its plan already lands below the band it is taken as it is (target "room"): it keeps the
75
- * room plan's content exactly, because every allowance of the planner is derived from the reserve and the band's is smaller. Only
76
- * otherwise is the band planned. Inside the band a cut is opportunistic, so a failed plan is never a fault and never falls back to the
77
- * room (a cut that still lands in the band would trigger again every round); the exact room would cut nothing there, so a band cut is
78
- * taken only when the room can plan a cut too and with the same mode (a lossy view the room could not even plan is a quality change with
79
- * no fault to prevent). At or above the room, where a cut is due, a band plan of another mode gives way to the room plan (today's
80
- * view), and a band plan stands when the room has none (a cut instead of a fault).
81
- * A lean band, one the cut point binds (`band < bandOf(room)`), is planned first and the room plan landing below it is not taken for that:
82
- * the room plan keeps a native tail of up to half the room, so it can land just under a band that far below the room, and the next round
83
- * would cut again, freeing little. The room is planned only where the rules above use it: to compare the mode, or when the band has no plan
84
- * and a cut is due. An adaptive band can also reach the margin, in which case the room plan is tried first.
85
- * The margin adds no new decision fault. Exceptions thrown by `plan` are not caught here: they are faults, and they now surface at
86
- * boundaries inside the band too, where no planner ran before the margin.
87
- */
86
+ /** Plan the band first at every cut point. An opportunistic cut must land
87
+ * below it and free the existing minimum. A due cut takes a band draft, or
88
+ * falls back to the room only when the band has no draft. Final reduction and
89
+ * room-fit checks own faults. Planner exceptions propagate. */
88
90
  export function chooseCut(input) {
89
91
  const { before, mandatory, room, band, fits, plan, unreduced } = input;
90
92
  const lands = (p) => p.draft && p.after < before && fits(p.after + mandatory, band);
91
93
  // A cut no one needs yet must be worth its summary: one that frees under a tenth of the room would trigger again within a round or two.
92
- // Without this, a band at the cap takes the room plan on every round where free room is small. A due cut is not asked: it has to happen.
94
+ // A due cut is not asked: it has to happen.
93
95
  const worthwhile = (p) => !input.usefulOnly || !input.opportunistic || !fits(10 * (before - p.after), room);
94
- let roomPlan;
95
- const exact = () => roomPlan ??= plan("room");
96
- if (band >= bandOf(room)) {
97
- const e = exact();
98
- if (lands(e) && worthwhile(e))
99
- return { kind: "cut", target: "room", plan: e };
100
- }
101
96
  const preferred = plan("band");
102
97
  if (input.opportunistic) {
103
98
  if (!lands(preferred) || !worthwhile(preferred))
104
99
  return { kind: "none" };
105
- const e = exact();
106
- return e.draft && e.mode === preferred.mode ? { kind: "cut", target: "band", plan: preferred } : { kind: "none" };
100
+ return { kind: "cut", target: "band", plan: preferred };
107
101
  }
108
- const e = exact();
109
102
  let chosen = preferred, target = "band";
110
- if (!preferred.draft || (e.draft && e.mode !== preferred.mode)) {
111
- chosen = e;
103
+ if (!preferred.draft) {
104
+ chosen = plan("room");
112
105
  target = "room";
113
106
  }
114
107
  if (!chosen.draft)
@@ -1 +1 @@
1
- export declare const BUILD_ID = "27bb99fa3ca595dac73089d18c4c3506073c5d3e3f778ea572708f6e7a49ffc3";
1
+ export declare const BUILD_ID = "3bbefa84bf1600296d12131a405f9feaa8fb1ea26ca1a85f5292b40a0a57333f";
@@ -1 +1 @@
1
- export const BUILD_ID = "27bb99fa3ca595dac73089d18c4c3506073c5d3e3f778ea572708f6e7a49ffc3";
1
+ export const BUILD_ID = "3bbefa84bf1600296d12131a405f9feaa8fb1ea26ca1a85f5292b40a0a57333f";