pi-tuck 1.0.26 → 1.0.28
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 +47 -0
- package/README.md +9 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +37 -24
- package/dist/src/build-id.d.ts +1 -1
- package/dist/src/build-id.js +1 -1
- package/dist/src/compaction.d.ts +4 -0
- package/dist/src/compaction.js +156 -172
- package/dist/src/diagnostics.d.ts +1 -1
- package/dist/src/request-capacity.d.ts +8 -3
- package/dist/src/request-capacity.js +19 -14
- package/dist/src/run.d.ts +52 -0
- package/dist/src/run.js +89 -0
- package/dist/src/semantic-recall.d.ts +72 -2
- package/dist/src/semantic-recall.js +58 -12
- package/dist/src/status.d.ts +1 -0
- package/dist/src/status.js +2 -1
- package/dist/src/tuck.js +33 -27
- package/index.ts +31 -22
- package/package.json +1 -1
- package/src/build-id.ts +1 -1
- package/src/compaction.ts +137 -155
- package/src/request-capacity.ts +18 -12
- package/src/run.ts +91 -0
- package/src/semantic-recall.ts +46 -14
- package/src/status.ts +3 -1
- package/src/tuck.ts +32 -25
package/dist/src/tuck.js
CHANGED
|
@@ -108,16 +108,17 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
108
108
|
return { unavailable: "No reducible history before the fresh shell output" };
|
|
109
109
|
cut = cutBefore(cut, freshBash, prefix);
|
|
110
110
|
}
|
|
111
|
-
//
|
|
112
|
-
// Explicit omission of it must not promote an earlier user source instead.
|
|
111
|
+
// The latest user source still owns first exposure and non-text native floors. An omission never promotes an older source.
|
|
113
112
|
const latestUser = retainCurrentInput ? branch.findLast(entry => entry.type === "message" && entry.message.role === "user") : undefined;
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
//
|
|
119
|
-
const
|
|
120
|
-
const
|
|
113
|
+
const latestPosition = entries.findIndex(entry => entry.id === latestUser?.id && entry.type === "message");
|
|
114
|
+
const latest = entries[latestPosition];
|
|
115
|
+
const latestContent = latest?.type === "message" && latest.message.role === "user" ? latest.message.content : undefined;
|
|
116
|
+
const latestTextOnly = isTextOnly(latestContent);
|
|
117
|
+
// During a run, current input is the run owner's starting entry, even after steering. Manual cuts use the latest source.
|
|
118
|
+
const currentId = input.protectedPromptEntryId ?? latestUser?.id;
|
|
119
|
+
const currentPosition = entries.findIndex(entry => entry.id === currentId &&
|
|
120
|
+
(entry.type === "custom_message" || entry.type === "message" && entry.message.role === "user"));
|
|
121
|
+
const current = entries[currentPosition];
|
|
121
122
|
// Anchor the actual first source only; a native omission never promotes a later prompt.
|
|
122
123
|
const firstUser = branch.find(entry => entry.type === "message" && entry.message.role === "user");
|
|
123
124
|
const firstUserPosition = entries.findIndex(entry => entry.id === firstUser?.id && entry.type === "message");
|
|
@@ -125,11 +126,11 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
125
126
|
const selectedBoundary = cut.compactAll ? entries.length : entries.findIndex(entry => entry.id === selectedKeptId);
|
|
126
127
|
// An unobserved input or non-text input stays native. Quoting cannot deliver
|
|
127
128
|
// its pixels/unknown blocks, and must not count as first exposure.
|
|
128
|
-
if (
|
|
129
|
-
const prefix = tuck.buildOwnCut(entries.slice(0,
|
|
129
|
+
if (latestPosition >= 0 && latestPosition < selectedBoundary && (!latestTextOnly || latestPosition > lastResponse)) {
|
|
130
|
+
const prefix = tuck.buildOwnCut(entries.slice(0, latestPosition), 0);
|
|
130
131
|
if (!prefix.ok)
|
|
131
132
|
return { unavailable: "No reducible history before the current user input" };
|
|
132
|
-
cut = cutBefore(cut,
|
|
133
|
+
cut = cutBefore(cut, latestPosition, prefix);
|
|
133
134
|
}
|
|
134
135
|
if (cut.messages.length !== cut.selectedIds.length || (!cut.compactAll && !entries.some((e) => e.id === cut.firstKeptEntryId)))
|
|
135
136
|
throw new Error("pi-tuck returned an invalid source boundary");
|
|
@@ -138,8 +139,8 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
138
139
|
let floorBoundary = freshRound ? lastResponse : entries.length;
|
|
139
140
|
if (freshBash >= 0)
|
|
140
141
|
floorBoundary = Math.min(floorBoundary, freshBash);
|
|
141
|
-
if (
|
|
142
|
-
floorBoundary = Math.min(floorBoundary,
|
|
142
|
+
if (latestPosition >= 0 && (!latestTextOnly || latestPosition > lastResponse))
|
|
143
|
+
floorBoundary = Math.min(floorBoundary, latestPosition);
|
|
143
144
|
// One fill owns the summary and the contiguous native suffix. Protocol/current-input
|
|
144
145
|
// floors are representation constraints; every other source competes by rank.
|
|
145
146
|
return rankedFill();
|
|
@@ -256,10 +257,12 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
256
257
|
if (cached)
|
|
257
258
|
return cached;
|
|
258
259
|
const index = indices.get(entry.id);
|
|
259
|
-
const text = entry.type === "message" && entry.message.role === "user" ? textOf(entry.message.content) : "";
|
|
260
|
+
const text = entry.type === "custom_message" ? textOf(entry.content) : entry.type === "message" && entry.message.role === "user" ? textOf(entry.message.content) : "";
|
|
260
261
|
const sourceTimes = index === undefined ? undefined : [{ sourceIndex: index, ...resolveSourceTime(entry) }];
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
// Custom entries have a native identity but no global message index.
|
|
263
|
+
const records = index === undefined && entry.type !== "custom_message" ? [] : take === Infinity
|
|
264
|
+
? [{ kind: "excerpt", role: entry.type === "custom_message" ? "custom" : "user", ...(index === undefined ? {} : { sourceIndex: index }), text, sourceTimes,
|
|
265
|
+
...(entry.type === "custom_message" ? { sourceEntryId: entry.id } : {}) }]
|
|
263
266
|
: userPrefixRecords({ kind: "user", text, originalText: text, startsSource: true, sourceIndex: index }, undefined, take).map(record => ({ ...record, sourceTimes }));
|
|
264
267
|
cached = { records, cost: lineCost(records), whole: take >= text.length };
|
|
265
268
|
quoteCache.set(key, cached);
|
|
@@ -278,12 +281,12 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
278
281
|
whole.add(index);
|
|
279
282
|
}
|
|
280
283
|
};
|
|
281
|
-
if (
|
|
284
|
+
if (latestPosition >= 0 && latestPosition < boundary && latestTextOnly)
|
|
285
|
+
add(latest, Infinity);
|
|
286
|
+
if (currentPosition >= 0 && currentPosition < boundary && current?.id !== latest?.id)
|
|
282
287
|
add(current, Infinity);
|
|
283
|
-
if (startingPosition >= 0 && startingPosition < boundary && starting?.id !== current?.id)
|
|
284
|
-
add(starting, Infinity);
|
|
285
288
|
for (const entry of anchors)
|
|
286
|
-
if (entry.id !==
|
|
289
|
+
if (entry.id !== latest?.id && entry.id !== current?.id && positions.get(entry.id) < boundary)
|
|
287
290
|
add(entry, length);
|
|
288
291
|
const omissions = [];
|
|
289
292
|
for (const role of ["user", "assistant", "toolResult"]) {
|
|
@@ -399,21 +402,24 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
399
402
|
if (row.message.role === "user" && Array.isArray(row.message.content))
|
|
400
403
|
imageLabels.set(row.index, new Set(row.message.content.flatMap(part => part.type === "image" ? [`[image: ${part.mimeType}]`] : [])));
|
|
401
404
|
const compiledBoundary = boundary;
|
|
405
|
+
// A protected custom prompt uses its native entry ID in the same anchoring/movement path as indexed user prompts.
|
|
406
|
+
const sourceKey = (record) => current?.type === "custom_message" && record.sourceEntryId === current.id ? current.id : record.sourceIndex;
|
|
402
407
|
const assemble = (compiled, at = compiledBoundary) => {
|
|
403
408
|
const split = compiled ? splitSummary(compiled) : undefined;
|
|
404
409
|
if (compiled && !split)
|
|
405
410
|
throw new Error("pi-tuck returned an invalid summary");
|
|
406
411
|
const inputRecords = split ? parseRecords(split.body, split.version) : protectedView.omissions;
|
|
407
|
-
const expanded = new Set(protectedView.kept.filter(record => record.kind === "excerpt" && inputRecords.some(row => row.kind === "excerpt" && row.role ===
|
|
408
|
-
const anchored = new Set(protectedView.kept.map(
|
|
409
|
-
const moved = new Set(rows.filter(row => row.position >= at && row.position < compiledBoundary).map(row => row.index));
|
|
412
|
+
const expanded = new Set(protectedView.kept.filter(record => record.kind === "excerpt" && inputRecords.some(row => row.kind === "excerpt" && row.role === record.role && sourceKey(row) === sourceKey(record) && row.text.startsWith(record.text) && row.text.length >= record.text.length)).map(sourceKey));
|
|
413
|
+
const anchored = new Set(protectedView.kept.map(sourceKey));
|
|
414
|
+
const moved = new Set(rows.filter(row => row.position >= at && row.position < compiledBoundary).map(row => current?.type === "custom_message" && row.id === current.id ? row.id : row.index));
|
|
410
415
|
const records = [...inputRecords.filter(record => {
|
|
411
|
-
|
|
416
|
+
const key = sourceKey(record);
|
|
417
|
+
if (key !== undefined && moved.has(key))
|
|
412
418
|
return false;
|
|
413
419
|
if (record.kind === "excerpt" && record.role === "user" && imageLabels.get(record.sourceIndex)?.has(record.text))
|
|
414
420
|
return true;
|
|
415
|
-
return record.role !== "user"
|
|
416
|
-
}), ...protectedView.kept.filter(record => !expanded.has(record
|
|
421
|
+
return record.role !== "user" && record.role !== "custom" || key === undefined || !anchored.has(key) || expanded.has(key);
|
|
422
|
+
}), ...protectedView.kept.filter(record => !expanded.has(sourceKey(record)) && !moved.has(sourceKey(record)))];
|
|
417
423
|
// Mixed user turns keep their text opening, while their image
|
|
418
424
|
// metadata remains in the ordinary lower-ranked dialogue lane.
|
|
419
425
|
const wholeUser = new Set(records.filter(record => record.kind === "excerpt" && record.role === "user" && record.text === userTexts.get(record.sourceIndex)).map(record => record.sourceIndex));
|
package/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type { CompactionOptions } from "./src/compaction";
|
|
|
11
11
|
export { installCompaction } from "./src/compaction";
|
|
12
12
|
export { inspectIncident, readableIncident } from "./src/diagnostic-replay.js";
|
|
13
13
|
export { createSemanticRecall, SemanticRecallController } from "./src/semantic-recall";
|
|
14
|
-
export type { SemanticRecallConfig, PriorResult, SemanticState } from "./src/semantic-recall";
|
|
14
|
+
export type { SemanticRecallConfig, PriorResult, SemanticState, SemanticSetup } from "./src/semantic-recall";
|
|
15
15
|
export { ensureSemanticRuntime } from "./src/setup";
|
|
16
16
|
|
|
17
17
|
/** pi-tuck as a Pi extension: compaction and history recall always; semantic recall too, unless `pi-tuck.json` in Pi's agent folder
|
|
@@ -41,13 +41,19 @@ export default async function (pi: ExtensionAPI) {
|
|
|
41
41
|
}
|
|
42
42
|
const semanticRecall = createSemanticRecall();
|
|
43
43
|
// `/tuck-semantic off` holds for the rest of the session, including over a setup that finishes later.
|
|
44
|
-
let off = false;
|
|
45
44
|
const settings = readSettings(join(getAgentDir(), "pi-tuck.json"));
|
|
46
45
|
// Semantic recall runs on Metal, so only on Apple silicon; elsewhere the model is not offered tuck_search at all.
|
|
47
46
|
const appleSilicon = process.platform === "darwin" && process.arch === "arm64";
|
|
48
|
-
const handle = await installCompaction(pi, { semanticRecall, search: appleSilicon, cutAt: settings.cutAt, onSemanticSetting: async enabled => {
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const handle = await installCompaction(pi, { semanticRecall, search: appleSilicon, cutAt: settings.cutAt, onSemanticSetting: async enabled => {
|
|
48
|
+
if (!enabled) semanticRecall.setSetup("off-user");
|
|
49
|
+
else if (semanticRecall.status().setup === "off-user") semanticRecall.setSetup("starting", { installed: existsSync(runtimeConfigPath(defaultRoot())) });
|
|
50
|
+
} });
|
|
51
|
+
if (settings.semanticRecall === false) {
|
|
52
|
+
semanticRecall.setSetup("off-settings");
|
|
53
|
+
pi.on("session_start", () => { semanticRecall.setSetup("off-settings"); });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let session = 0, said = "", announced = false, setup: Promise<unknown> | undefined;
|
|
51
57
|
// A context outlives its session (a setup can finish after /new); a notice to an ended session is dropped.
|
|
52
58
|
// 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
59
|
// instead, once per session. stderr is the operator's; it never enters the model's context.
|
|
@@ -57,41 +63,44 @@ export default async function (pi: ExtensionAPI) {
|
|
|
57
63
|
try { ctx.ui.notify(message, level); } catch { /* ended */ }
|
|
58
64
|
};
|
|
59
65
|
const once = (ctx: ExtensionContext, message: string, level: "info" | "warning") => { if (said !== message) { said = message; say(ctx, message, level); } };
|
|
66
|
+
const announce = (ctx: ExtensionContext, state: Parameters<typeof semanticRecall.setSetup>[0], detail?: Parameters<typeof semanticRecall.setSetup>[1]) => {
|
|
67
|
+
semanticRecall.setSetup(state, detail);
|
|
68
|
+
const notice = semanticRecall.setupNotice();
|
|
69
|
+
if (notice) (state === "off-platform" || state === "off-project" ? once : say)(ctx, notice.message, notice.level);
|
|
70
|
+
};
|
|
60
71
|
pi.on("session_shutdown", () => { session++; });
|
|
61
72
|
pi.on("session_start", (_event, ctx) => {
|
|
62
73
|
const current = ++session;
|
|
63
|
-
off = false;
|
|
64
74
|
stderrSaid.clear();
|
|
65
75
|
if (settings.error) once(ctx, `pi-tuck: ${settings.error}; using the defaults.`, "warning");
|
|
66
76
|
if (!appleSilicon)
|
|
67
|
-
return
|
|
77
|
+
return announce(ctx, "off-platform");
|
|
68
78
|
// Semantic recall keeps its runtime and indexes outside the project it serves, so not when Pi runs in a folder that holds its
|
|
69
79
|
// agent folder, such as the home folder.
|
|
70
80
|
if (within(real(getAgentDir()), real(ctx.cwd)))
|
|
71
|
-
return
|
|
81
|
+
return announce(ctx, "off-project");
|
|
72
82
|
// 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
83
|
// setup, which would outlast a one-shot run, and it says why, so tuck_search and /tuck name the real state.
|
|
74
84
|
const installed = existsSync(runtimeConfigPath(defaultRoot()));
|
|
75
|
-
if (!ctx.hasUI && !installed)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
if (!ctx.hasUI && !installed) return announce(ctx, "not-installed");
|
|
86
|
+
// The download's announcement outlives the session that made it: a session that starts while the shared setup is still running
|
|
87
|
+
// owes the "set up" notice once, as before the refactor.
|
|
88
|
+
semanticRecall.setSetup("starting", { installed, announced });
|
|
89
|
+
setup ??= ensureSemanticRuntime(defaultRoot(), message => {
|
|
90
|
+
announced = true;
|
|
91
|
+
if (current === session && semanticRecall.status().setup !== "off-user") announce(ctx, "starting", { installed, notice: message, announced: true });
|
|
92
|
+
});
|
|
82
93
|
setup.then(async () => {
|
|
83
|
-
if (current !== session || off) return;
|
|
94
|
+
if (current !== session || semanticRecall.status().setup === "off-user") return;
|
|
84
95
|
try {
|
|
85
|
-
await handle.setSemanticEnabled(true, ctx);
|
|
86
|
-
|
|
87
|
-
if (announced && current === session) { announced = false; say(ctx, "pi-tuck: semantic recall is set up. Search by meaning works from the first cut on.", "info"); }
|
|
96
|
+
await handle.setSemanticEnabled(true, ctx);
|
|
97
|
+
if (current === session && semanticRecall.status().setup !== "off-user") { announce(ctx, "ready", { announced }); announced = false; }
|
|
88
98
|
} catch (error) {
|
|
89
|
-
if (current === session)
|
|
99
|
+
if (current === session && semanticRecall.status().setup !== "off-user") announce(ctx, "failed", { stage: "start", reason: reason(error) });
|
|
90
100
|
}
|
|
91
101
|
}, error => {
|
|
92
102
|
setup = undefined; // the next session start tries again
|
|
93
|
-
semanticRecall.
|
|
94
|
-
if (current === session) say(ctx, `pi-tuck: semantic recall setup failed (${reason(error)}). Word recall works; setup retries when a session starts.`, "warning");
|
|
103
|
+
if (current === session && semanticRecall.status().setup !== "off-user") announce(ctx, "failed", { stage: "setup", reason: reason(error) });
|
|
95
104
|
});
|
|
96
105
|
});
|
|
97
106
|
}
|
package/package.json
CHANGED
package/src/build-id.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const BUILD_ID = "
|
|
1
|
+
export const BUILD_ID = "e9c750b757776bf44ad79939b633f178ef9a8f14acfab1c8bb6311d692c79e61";
|