pi-tuck 1.0.19 → 1.0.21
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 +41 -0
- package/README.md +11 -7
- package/dist/index.js +30 -9
- package/dist/src/build-id.d.ts +1 -1
- package/dist/src/build-id.js +1 -1
- package/dist/src/compaction.d.ts +3 -3
- package/dist/src/compaction.js +9 -7
- package/dist/src/diagnostics.d.ts +1 -1
- package/index.ts +19 -5
- package/package.json +1 -1
- package/src/build-id.ts +1 -1
- package/src/compaction.ts +13 -11
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,47 @@
|
|
|
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.21] - 2026-11-09
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **Semantic recall never started in `pi -p` (1.0.0 to 1.0.20).** pi-tuck skipped semantic recall whenever Pi ran with no UI, to keep a one-shot
|
|
11
|
+
run from starting the first-time download. That also skipped a runtime that was already installed. So jobs run with `pi -p` searched by keyword
|
|
12
|
+
only, while `tuck_search` said semantic recall was being set up. With no UI, an installed runtime now starts as it does in an interactive session.
|
|
13
|
+
Without one, pi-tuck says it is not set up and starts no download. RPC mode, which has a UI, was not affected.
|
|
14
|
+
- **An off or failing semantic recall, or a bad settings file, is no longer silent in `pi -p`.** Pi shows no notices there, so pi-tuck writes each
|
|
15
|
+
such message once per session to stderr, never into the model's context.
|
|
16
|
+
|
|
17
|
+
### Release gates
|
|
18
|
+
|
|
19
|
+
- **Shipped without a live gate, because its replay matches 1.0.20.** Replay does not load the changed file (`index.ts`). In the replay, 43 requests
|
|
20
|
+
of one recording (live-s08-A) differed once. Three more runs of that recording on each build were identical, so it was noise under machine load.
|
|
21
|
+
0 lost and 0 dead tasks, the same 17 clips. Tests and CI; the settings stress test, none worse than 1.0.20.
|
|
22
|
+
- Checked in real Pi 1.4.0: with `pi -p` and no runtime, one stderr line; with an installed runtime, semantic recall starts.
|
|
23
|
+
- Sessions that are open keep the build they started with until they are restarted.
|
|
24
|
+
|
|
25
|
+
## [1.0.20] - 2026-11-09
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **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`
|
|
30
|
+
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
|
|
31
|
+
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
|
|
32
|
+
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
|
|
33
|
+
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
|
|
34
|
+
at `reserveTokens` again. A host that passes its own reserve keeps today's behaviour.
|
|
35
|
+
- Lowering `reserveTokens` now takes effect even when the model's `maxTokens` is larger, until a reply does not fit.
|
|
36
|
+
|
|
37
|
+
### Release gates
|
|
38
|
+
|
|
39
|
+
- **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
|
|
40
|
+
17 clips. Every recording has the reply cap equal to the reserve, where nothing changes.
|
|
41
|
+
- 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
|
|
42
|
+
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
|
|
43
|
+
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
|
|
44
|
+
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.
|
|
45
|
+
- Sessions that are open keep the build they started with until they are restarted.
|
|
46
|
+
|
|
6
47
|
## [1.0.19] - 2026-11-09
|
|
7
48
|
|
|
8
49
|
### 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.
|
|
68
|
-
- To pin a version, add it: `pi install npm:pi-tuck@1.0.
|
|
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.20`.
|
|
68
|
+
- To pin a version, add it: `pi install npm:pi-tuck@1.0.20`.
|
|
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,
|
|
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
|
|
|
@@ -125,9 +126,10 @@ Known limits today:
|
|
|
125
126
|
|
|
126
127
|
- pi-tuck's summary does not explain why things happened. It holds the words that were said, not a model's reading of them. The agent must look things up. Each look-up costs a tool call.
|
|
127
128
|
- 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).
|
|
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
|
|
129
|
+
- 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 was 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.
|
|
129
130
|
- 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).
|
|
130
131
|
- 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).
|
|
132
|
+
- pi-tuck cut earlier than native Pi when a model's reply cap is above Pi's reserve (1.0.0 to 1.0.19; fixed in 1.0.20). It reserved the larger of Pi's `compaction.reserveTokens` 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 compacts at 65,536 ([`CHANGELOG.md`](CHANGELOG.md) 1.0.20).
|
|
131
133
|
- 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
134
|
- 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
135
|
- 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).
|
|
@@ -183,13 +185,15 @@ A bad file is reported at session start, and pi-tuck uses the defaults.
|
|
|
183
185
|
|
|
184
186
|
### Small windows
|
|
185
187
|
|
|
186
|
-
pi-tuck keeps room for the model's reply
|
|
188
|
+
pi-tuck keeps room for the model's reply. Since 1.0.20 that room is Pi's `compaction.reserveTokens` (default 16,384), the same as native Pi. It rises to the model's `maxTokens` for the rest of the branch only after a reply on that branch did not fit: a reply that stopped for length, or a request refused as a context overflow. A transient error such as a 503 keeps the native room. This is read from the current branch at each decision, so resume, fork and `/tree` follow the branch, and a new session starts at `reserveTokens` again. A host that passes its own reserve keeps its own. Before 1.0.20, pi-tuck always reserved the larger of the two, so with a large `maxTokens` it cut earlier than native Pi.
|
|
189
|
+
|
|
190
|
+
pi-tuck refuses a request that cannot fit, rather than let the provider cut it off. It estimates the request's size, and it scales that estimate by the worst gap it has seen between its count and the provider's. In a small window, an upstream that spends far more context on tool schemas (some OpenRouter upstreams do) can leave too little room. pi-tuck then refuses to cut rather than overflow. Lower the reserve for that model in Pi's `settings.json`, or use a larger window. For example, to lower it to 8,192:
|
|
187
191
|
|
|
188
192
|
```json
|
|
189
193
|
{ "compaction": { "modelOverrides": { "my-provider/my-model": { "reserveTokens": 8192 } } } }
|
|
190
194
|
```
|
|
191
195
|
|
|
192
|
-
|
|
196
|
+
Lowering `reserveTokens` takes effect even when `models.json` sets a larger `maxTokens` for that model, until a reply on the branch does not fit. After that, the larger of the two applies for the rest of the branch, so lower both if needed, and keep `maxTokens` above your largest expected reply.
|
|
193
197
|
|
|
194
198
|
If you reach the model through OpenRouter, you can also pin one upstream, so that every request is served by the same one. Put this in `models.json` in Pi's agent folder, and name the upstream you want:
|
|
195
199
|
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { VERSION, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { join, sep } from "node:path";
|
|
4
4
|
import { installCompaction } from "./src/compaction.js";
|
|
5
5
|
import { validCutAt } from "./src/budget.js";
|
|
6
6
|
import { createSemanticRecall } from "./src/semantic-recall.js";
|
|
7
|
-
import { defaultRoot } from "./src/assets.js";
|
|
7
|
+
import { defaultRoot, runtimeConfigPath } from "./src/assets.js";
|
|
8
8
|
import { ensureSemanticRuntime } from "./src/setup.js";
|
|
9
9
|
export { installCompaction } from "./src/compaction.js";
|
|
10
10
|
export { inspectIncident, readableIncident } from "./src/diagnostic-replay.js";
|
|
@@ -52,10 +52,25 @@ export default async function (pi) {
|
|
|
52
52
|
return;
|
|
53
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
// With no UI (pi -p), Pi shows no notices, so a settings error or an off or failing semantic recall would go unsaid: write each to stderr
|
|
56
|
+
// instead, once per session. stderr is the operator's; it never enters the model's context.
|
|
57
|
+
const stderrSaid = new Set();
|
|
58
|
+
const say = (ctx, message, level) => {
|
|
59
|
+
if (!ctx.hasUI) {
|
|
60
|
+
if (!stderrSaid.has(message)) {
|
|
61
|
+
stderrSaid.add(message);
|
|
62
|
+
try {
|
|
63
|
+
process.stderr.write(message + "\n");
|
|
64
|
+
}
|
|
65
|
+
catch { /* closed */ }
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
ctx.ui.notify(message, level);
|
|
71
|
+
}
|
|
72
|
+
catch { /* ended */ }
|
|
73
|
+
};
|
|
59
74
|
const once = (ctx, message, level) => { if (said !== message) {
|
|
60
75
|
said = message;
|
|
61
76
|
say(ctx, message, level);
|
|
@@ -64,6 +79,7 @@ export default async function (pi) {
|
|
|
64
79
|
pi.on("session_start", (_event, ctx) => {
|
|
65
80
|
const current = ++session;
|
|
66
81
|
off = false;
|
|
82
|
+
stderrSaid.clear();
|
|
67
83
|
if (settings.error)
|
|
68
84
|
once(ctx, `pi-tuck: ${settings.error}; using the defaults.`, "warning");
|
|
69
85
|
if (!appleSilicon)
|
|
@@ -72,10 +88,15 @@ export default async function (pi) {
|
|
|
72
88
|
// agent folder, such as the home folder.
|
|
73
89
|
if (within(real(getAgentDir()), real(ctx.cwd)))
|
|
74
90
|
return once(ctx, "pi-tuck: semantic recall is off here because Pi's agent folder is inside this folder; open Pi in a project folder to use it.", "info");
|
|
75
|
-
|
|
76
|
-
|
|
91
|
+
// A run with no UI (pi -p, RPC, JSON) uses a runtime that is already installed: that needs no download. It does not start a first-time
|
|
92
|
+
// setup, which would outlast a one-shot run, and it says why, so tuck_search and /tuck name the real state.
|
|
93
|
+
const installed = existsSync(runtimeConfigPath(defaultRoot()));
|
|
94
|
+
if (!ctx.hasUI && !installed) {
|
|
95
|
+
semanticRecall.refuse("Semantic recall is not set up yet; start Pi once in an interactive session to set it up. Word recall works meanwhile");
|
|
96
|
+
return say(ctx, "pi-tuck: semantic recall is not set up yet; start Pi once in an interactive session to set it up. Word recall works.", "warning");
|
|
97
|
+
}
|
|
77
98
|
if (!semanticRecall.status().configured)
|
|
78
|
-
semanticRecall.refuse("Semantic recall is being set up; word recall works meanwhile");
|
|
99
|
+
semanticRecall.refuse(installed ? "Semantic recall is starting; word recall works meanwhile" : "Semantic recall is being set up; word recall works meanwhile");
|
|
79
100
|
setup ??= ensureSemanticRuntime(defaultRoot(), message => { announced = true; say(ctx, message, "info"); });
|
|
80
101
|
setup.then(async () => {
|
|
81
102
|
if (current !== session || off)
|
package/dist/src/build-id.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const BUILD_ID = "
|
|
1
|
+
export declare const BUILD_ID = "82df9e9954f1d33eb2d6c6c653220c13bd3c36c2f0250afa2a17ab51295cfb0c";
|
package/dist/src/build-id.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const BUILD_ID = "
|
|
1
|
+
export const BUILD_ID = "82df9e9954f1d33eb2d6c6c653220c13bd3c36c2f0250afa2a17ab51295cfb0c";
|
package/dist/src/compaction.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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;
|
package/dist/src/compaction.js
CHANGED
|
@@ -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
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
//
|
|
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
|
|
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
|
-
|
|
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 = "
|
|
3
|
+
export declare const DIAGNOSTIC_BUILD = "82df9e9954f1d33eb2d6c6c653220c13bd3c36c2f0250afa2a17ab51295cfb0c";
|
|
4
4
|
export interface Receipt {
|
|
5
5
|
id: string;
|
|
6
6
|
build: string;
|
package/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { VERSION, getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { join, sep } from "node:path";
|
|
4
4
|
import { installCompaction } from "./src/compaction";
|
|
5
5
|
import { validCutAt } from "./src/budget";
|
|
6
6
|
import { createSemanticRecall } from "./src/semantic-recall";
|
|
7
|
-
import { defaultRoot } from "./src/assets";
|
|
7
|
+
import { defaultRoot, runtimeConfigPath } from "./src/assets";
|
|
8
8
|
import { ensureSemanticRuntime } from "./src/setup";
|
|
9
9
|
|
|
10
10
|
export type { CompactionOptions } from "./src/compaction";
|
|
@@ -49,12 +49,19 @@ export default async function (pi: ExtensionAPI) {
|
|
|
49
49
|
if (settings.semanticRecall === false) return;
|
|
50
50
|
let session = 0, said = "", setup: Promise<unknown> | undefined, announced = false;
|
|
51
51
|
// A context outlives its session (a setup can finish after /new); a notice to an ended session is dropped.
|
|
52
|
-
|
|
52
|
+
// With no UI (pi -p), Pi shows no notices, so a settings error or an off or failing semantic recall would go unsaid: write each to stderr
|
|
53
|
+
// instead, once per session. stderr is the operator's; it never enters the model's context.
|
|
54
|
+
const stderrSaid = new Set<string>();
|
|
55
|
+
const say = (ctx: ExtensionContext, message: string, level: "info" | "warning") => {
|
|
56
|
+
if (!ctx.hasUI) { if (!stderrSaid.has(message)) { stderrSaid.add(message); try { process.stderr.write(message + "\n"); } catch { /* closed */ } } return; }
|
|
57
|
+
try { ctx.ui.notify(message, level); } catch { /* ended */ }
|
|
58
|
+
};
|
|
53
59
|
const once = (ctx: ExtensionContext, message: string, level: "info" | "warning") => { if (said !== message) { said = message; say(ctx, message, level); } };
|
|
54
60
|
pi.on("session_shutdown", () => { session++; });
|
|
55
61
|
pi.on("session_start", (_event, ctx) => {
|
|
56
62
|
const current = ++session;
|
|
57
63
|
off = false;
|
|
64
|
+
stderrSaid.clear();
|
|
58
65
|
if (settings.error) once(ctx, `pi-tuck: ${settings.error}; using the defaults.`, "warning");
|
|
59
66
|
if (!appleSilicon)
|
|
60
67
|
return once(ctx, "pi-tuck: semantic recall needs macOS on Apple silicon; compaction and word recall work.", "info");
|
|
@@ -62,8 +69,15 @@ export default async function (pi: ExtensionAPI) {
|
|
|
62
69
|
// agent folder, such as the home folder.
|
|
63
70
|
if (within(real(getAgentDir()), real(ctx.cwd)))
|
|
64
71
|
return once(ctx, "pi-tuck: semantic recall is off here because Pi's agent folder is inside this folder; open Pi in a project folder to use it.", "info");
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
// A run with no UI (pi -p, RPC, JSON) uses a runtime that is already installed: that needs no download. It does not start a first-time
|
|
73
|
+
// setup, which would outlast a one-shot run, and it says why, so tuck_search and /tuck name the real state.
|
|
74
|
+
const installed = existsSync(runtimeConfigPath(defaultRoot()));
|
|
75
|
+
if (!ctx.hasUI && !installed) {
|
|
76
|
+
semanticRecall.refuse("Semantic recall is not set up yet; start Pi once in an interactive session to set it up. Word recall works meanwhile");
|
|
77
|
+
return say(ctx, "pi-tuck: semantic recall is not set up yet; start Pi once in an interactive session to set it up. Word recall works.", "warning");
|
|
78
|
+
}
|
|
79
|
+
if (!semanticRecall.status().configured)
|
|
80
|
+
semanticRecall.refuse(installed ? "Semantic recall is starting; word recall works meanwhile" : "Semantic recall is being set up; word recall works meanwhile");
|
|
67
81
|
setup ??= ensureSemanticRuntime(defaultRoot(), message => { announced = true; say(ctx, message, "info"); });
|
|
68
82
|
setup.then(async () => {
|
|
69
83
|
if (current !== session || off) return;
|
package/package.json
CHANGED
package/src/build-id.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const BUILD_ID = "
|
|
1
|
+
export const BUILD_ID = "82df9e9954f1d33eb2d6c6c653220c13bd3c36c2f0250afa2a17ab51295cfb0c";
|
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
|
-
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
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
|
-
|
|
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;
|