pi-tuck 1.0.12 → 1.0.14
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 +44 -0
- package/README.md +11 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/src/budget.d.ts +11 -6
- package/dist/src/budget.js +13 -9
- package/dist/src/build-id.d.ts +1 -1
- package/dist/src/build-id.js +1 -1
- package/dist/src/compaction.d.ts +2 -2
- package/dist/src/compaction.js +23 -14
- package/dist/src/diagnostics.d.ts +1 -1
- package/dist/src/omissions.d.ts +0 -1
- package/dist/src/omissions.js +9 -11
- package/dist/src/semantic-recall.js +1 -2
- package/dist/src/tuck/core/global-indices.d.ts +4 -2
- package/dist/src/tuck/core/global-indices.js +21 -35
- package/dist/src/tuck/core/normalize.d.ts +1 -1
- package/dist/src/tuck/core/normalize.js +4 -6
- package/dist/src/tuck/cut.js +13 -40
- package/dist/src/tuck.js +11 -15
- package/index.ts +1 -1
- package/package.json +1 -1
- package/src/budget.ts +15 -9
- package/src/build-id.ts +1 -1
- package/src/compaction.ts +25 -16
- package/src/omissions.ts +9 -11
- package/src/semantic-recall.ts +1 -2
- package/src/tuck/core/global-indices.ts +19 -33
- package/src/tuck/core/normalize.ts +6 -8
- package/src/tuck/cut.ts +11 -35
- package/src/tuck.ts +11 -15
|
@@ -52,7 +52,7 @@ const normalizeOne = (msg, msgIndex) => {
|
|
|
52
52
|
};
|
|
53
53
|
/**
|
|
54
54
|
* Normalize messages to blocks. `sourceIndices`, when provided, supplies the
|
|
55
|
-
* session-global `#N` index per input position (see
|
|
55
|
+
* session-global `#N` index per input position (see global-indices.ts).
|
|
56
56
|
* A missing entry yields `sourceIndex: undefined`, which downstream renderers
|
|
57
57
|
* display as no ref — never a window-relative number. Omitted entirely, the
|
|
58
58
|
* legacy positional behavior is preserved (used by callers/tests that have no
|
|
@@ -65,7 +65,6 @@ export const normalize = (messages, sourceIndices, receiptContext, sourceEntryId
|
|
|
65
65
|
if (sourceTimes && sourceTimes.length !== messages.length)
|
|
66
66
|
throw new Error("Original times must align with compiler messages");
|
|
67
67
|
const timeByIndex = Object.fromEntries((sourceIndices ?? messages.map((_, i) => i)).flatMap((index, i) => index === undefined || !sourceTimes ? [] : [[index, sourceTimes[i]]]));
|
|
68
|
-
const context = receiptContext ?? { messages, sourceIndices: sourceIndices ?? messages.map((_, i) => i) };
|
|
69
68
|
const pairs = new Map();
|
|
70
69
|
const group = (id) => {
|
|
71
70
|
if (typeof id !== "string" || !id)
|
|
@@ -77,17 +76,16 @@ export const normalize = (messages, sourceIndices, receiptContext, sourceEntryId
|
|
|
77
76
|
}
|
|
78
77
|
return pair;
|
|
79
78
|
};
|
|
80
|
-
|
|
79
|
+
(receiptContext?.messages ?? messages).forEach((msg, order) => {
|
|
81
80
|
if (msg.role === "assistant" && Array.isArray(msg.content))
|
|
82
81
|
for (const part of msg.content) {
|
|
83
82
|
if (part.type === "toolCall")
|
|
84
|
-
group(part.id).calls.push({ name: part.name, order,
|
|
83
|
+
group(part.id).calls.push({ name: part.name, order, args: part.arguments });
|
|
85
84
|
}
|
|
86
85
|
if (msg.role === "toolResult") {
|
|
87
86
|
if (typeof msg.isError !== "boolean")
|
|
88
87
|
throw new Error("Tool receipt lacks a recorded error status");
|
|
89
|
-
group(msg.toolCallId).receipts.push({ name: msg.toolName,
|
|
90
|
-
order, index: context.sourceIndices[order], isError: msg.isError, content: msg.content });
|
|
88
|
+
group(msg.toolCallId).receipts.push({ name: msg.toolName, order });
|
|
91
89
|
}
|
|
92
90
|
});
|
|
93
91
|
// A call paired with exactly one later receipt of the same tool has a recorded outcome. An export through recall keeps its outcome block.
|
package/dist/src/tuck/cut.js
CHANGED
|
@@ -34,47 +34,20 @@ const toLiveMessage = (entry) => {
|
|
|
34
34
|
return null;
|
|
35
35
|
};
|
|
36
36
|
const collectLiveMessages = (branchEntries) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
lastKeptId = branchEntries[i].firstKeptEntryId;
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
// Orphan recovery: triggers when lastKeptId is set to "" (sentinel from prior
|
|
48
|
-
// compact-all) OR set to an id that no longer exists in the branch. In both cases,
|
|
49
|
-
// start collecting from right after the last compaction entry.
|
|
50
|
-
const hasPriorCompaction = lastCompactionIdx >= 0;
|
|
51
|
-
const hasValidKeptId = !!lastKeptId && branchEntries.some((e) => e.id === lastKeptId);
|
|
52
|
-
const orphanRecovery = hasPriorCompaction && !hasValidKeptId;
|
|
53
|
-
// Collect live messages
|
|
37
|
+
const lastCompactionIdx = branchEntries.findLastIndex(e => e.type === "compaction");
|
|
38
|
+
const lastKeptId = branchEntries[lastCompactionIdx]?.firstKeptEntryId;
|
|
39
|
+
const kept = lastKeptId ? branchEntries.findIndex(e => e.id === lastKeptId) : -1;
|
|
40
|
+
// An empty compact-all sentinel or a missing anchor resumes after the last
|
|
41
|
+
// compaction. With no prior compaction, collection starts at the beginning.
|
|
42
|
+
const start = kept >= 0 ? kept : lastCompactionIdx + 1;
|
|
54
43
|
const liveMessages = [];
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
liveMessages.push({ entry: e, message: m });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
let foundKept = !lastKeptId; // if no prior compaction, start collecting immediately
|
|
67
|
-
for (const e of branchEntries) {
|
|
68
|
-
if (!foundKept && e.id === lastKeptId)
|
|
69
|
-
foundKept = true;
|
|
70
|
-
if (!foundKept)
|
|
71
|
-
continue;
|
|
72
|
-
if (e.type === "compaction")
|
|
73
|
-
continue;
|
|
74
|
-
const m = toLiveMessage(e);
|
|
75
|
-
if (m)
|
|
76
|
-
liveMessages.push({ entry: e, message: m });
|
|
77
|
-
}
|
|
44
|
+
for (let i = start; i < branchEntries.length; i++) {
|
|
45
|
+
const e = branchEntries[i];
|
|
46
|
+
if (e.type === "compaction")
|
|
47
|
+
continue;
|
|
48
|
+
const m = toLiveMessage(e);
|
|
49
|
+
if (m)
|
|
50
|
+
liveMessages.push({ entry: e, message: m });
|
|
78
51
|
}
|
|
79
52
|
return liveMessages;
|
|
80
53
|
};
|
package/dist/src/tuck.js
CHANGED
|
@@ -53,8 +53,8 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
53
53
|
const chars = (calibration.ok ? calibration.messages.reduce((n, m) => n + estimateSourceMessageChars(m), 0) : 0)
|
|
54
54
|
+ (input.previousSummary?.length ?? 0);
|
|
55
55
|
const { charsPerToken } = tuck.calibrateCharsPerToken(chars, input.tokensBefore);
|
|
56
|
-
//
|
|
57
|
-
//
|
|
56
|
+
// The planner's reserve includes mandatory input (system, host and incoming prompt), so this is half of the target room *after* that
|
|
57
|
+
// fixed input, not half of the whole room. Current/fresh-source floors below may still require an explicit rejection.
|
|
58
58
|
const inputRoom = Math.floor(Math.max(0, (ctx.model?.contextWindow ?? 0) - reserveTokens - 1) / 2);
|
|
59
59
|
const maxTokens = Math.min(MAX_TAIL_TOKENS, inputRoom);
|
|
60
60
|
// Keep the current user turn native; older conversation competes in the
|
|
@@ -71,6 +71,11 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
71
71
|
cut = { ok: true, messages: [], selectedIds: [], firstKeptEntryId: "", compactAll: true, keptUserTurns: 0, keepFallbackToCompactAll: false };
|
|
72
72
|
if (!cut.ok)
|
|
73
73
|
return { unavailable: `pi-tuck cannot select a cut: ${cut.reason}` };
|
|
74
|
+
const cutBefore = (cut, boundary, prefix) => ({
|
|
75
|
+
...cut, messages: prefix.messages, selectedIds: prefix.selectedIds,
|
|
76
|
+
firstKeptEntryId: entries[boundary]?.id ?? "", compactAll: boundary === entries.length, keepFallbackToCompactAll: false,
|
|
77
|
+
keptUserTurns: entries.slice(boundary).filter(entry => entry.type === "message" && entry.message.role === "user").length,
|
|
78
|
+
});
|
|
74
79
|
// The latest call's trailing receipts have not reached a subsequent response.
|
|
75
80
|
// A preferred tail size must not erase this first exposure when the whole
|
|
76
81
|
// fresh round fits. Existing full-draft admission rejects an impossible round.
|
|
@@ -93,9 +98,7 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
93
98
|
const prefix = tuck.buildOwnCut(entries.slice(0, lastResponse), 0);
|
|
94
99
|
if (!prefix.ok)
|
|
95
100
|
return { unavailable: "No reducible history before the fresh tool round" };
|
|
96
|
-
cut =
|
|
97
|
-
firstKeptEntryId: response.id, compactAll: false, keepFallbackToCompactAll: false,
|
|
98
|
-
keptUserTurns: entries.slice(lastResponse).filter(entry => entry.type === "message" && entry.message.role === "user").length };
|
|
101
|
+
cut = cutBefore(cut, lastResponse, prefix);
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
// Direct shell output after the last response has not reached the model.
|
|
@@ -108,9 +111,7 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
108
111
|
const prefix = tuck.buildOwnCut(entries.slice(0, freshBash), 0);
|
|
109
112
|
if (!prefix.ok)
|
|
110
113
|
return { unavailable: "No reducible history before the fresh shell output" };
|
|
111
|
-
cut =
|
|
112
|
-
firstKeptEntryId: entries[freshBash].id, compactAll: false, keepFallbackToCompactAll: false,
|
|
113
|
-
keptUserTurns: entries.slice(freshBash).filter(entry => entry.type === "message" && entry.message.role === "user").length };
|
|
114
|
+
cut = cutBefore(cut, freshBash, prefix);
|
|
114
115
|
}
|
|
115
116
|
// Preserve the actual latest user source, never an inferred task/decision.
|
|
116
117
|
// Explicit omission of it must not promote an earlier user source instead.
|
|
@@ -131,9 +132,7 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
131
132
|
const prefix = tuck.buildOwnCut(entries.slice(0, userPosition), 0);
|
|
132
133
|
if (!prefix.ok)
|
|
133
134
|
return { unavailable: "No reducible history before the current user input" };
|
|
134
|
-
cut =
|
|
135
|
-
firstKeptEntryId: current.id, compactAll: false, keepFallbackToCompactAll: false,
|
|
136
|
-
keptUserTurns: entries.slice(userPosition).filter(entry => entry.type === "message" && entry.message.role === "user").length };
|
|
135
|
+
cut = cutBefore(cut, userPosition, prefix);
|
|
137
136
|
}
|
|
138
137
|
if (cut.messages.length !== cut.selectedIds.length || (!cut.compactAll && !entries.some((e) => e.id === cut.firstKeptEntryId)))
|
|
139
138
|
throw new Error("pi-tuck returned an invalid source boundary");
|
|
@@ -272,10 +271,7 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
272
271
|
const prefix = tuck.buildOwnCut(entries.slice(0, floorBoundary), 0);
|
|
273
272
|
if (!prefix.ok)
|
|
274
273
|
return preferred;
|
|
275
|
-
const retried = compileAtCut({ ...cut,
|
|
276
|
-
firstKeptEntryId: entries[floorBoundary]?.id ?? "", compactAll: floorBoundary === entries.length,
|
|
277
|
-
keepFallbackToCompactAll: false,
|
|
278
|
-
keptUserTurns: entries.slice(floorBoundary).filter(entry => entry.type === "message" && entry.message.role === "user").length });
|
|
274
|
+
const retried = compileAtCut({ ...cutBefore(cut, floorBoundary, prefix), budgetCut: undefined });
|
|
279
275
|
return preferred.compaction && retried.mode !== "dialogue" ? preferred : retried;
|
|
280
276
|
}
|
|
281
277
|
/** Numeric references use the original session index, then intersect with readable current-branch entries. */
|
package/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ 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
|
|
18
18
|
* says `{"semanticRecall": false}`. Its runtime is set up in the background the first time a session starts. The same file's `"cutAt"`
|
|
19
|
-
* (a number above 0 and at most 1) is the fraction of the input room a cut is triggered at,
|
|
19
|
+
* (a number above 0 and at most 1) is the fraction of the input room a cut is triggered at, 0.6 by default. */
|
|
20
20
|
/** The oldest Pi with every hook and API pi-tuck uses, and the oldest its tests pass on (Pi 1.2 added the custom message that starts a
|
|
21
21
|
* run to before_agent_start, which the prompt's cut counts). */
|
|
22
22
|
export const PI_FLOOR = "1.2.0";
|
package/package.json
CHANGED
package/src/budget.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { convertToLlm, estimateTokens, type BeforeAgentStartEvent, type Extensio
|
|
|
3
3
|
import { measurableMessages, projectedMessages } from "./omissions.js";
|
|
4
4
|
import type { RecallPolicy } from "./recall.js";
|
|
5
5
|
import { isDiscardedResponse } from "./protocol.js";
|
|
6
|
+
import { MAX_TAIL_TOKENS } from "./tuck/cut.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The one measure of the next request. The cut triggers, the cut planner (including the draft admission in tuck.ts), the post-cut
|
|
@@ -19,16 +20,16 @@ export const TRIGGER_MARGIN_FRACTION = 0.02;
|
|
|
19
20
|
/** Whether `value` is a cut point: a fraction of the room above 0 and at most 1. */
|
|
20
21
|
export const validCutAt = (value: unknown): value is number => typeof value === "number" && value > 0 && value <= 1;
|
|
21
22
|
/**
|
|
22
|
-
* The one owner of the band: the limit a cut is triggered at and preferably planned to.
|
|
23
|
-
*
|
|
24
|
-
* whatever the cut point. With `cutAt` 1 the band is the room less the margin alone.
|
|
23
|
+
* The one owner of the band: the limit a cut is triggered at and preferably planned to. An adaptive default may raise the cut point above
|
|
24
|
+
* 60% using the last cut's conversation size and current system size, both estimates, so they are converted to the room's units by the factor. Explicit cut points use the fraction alone. The margin caps both.
|
|
25
25
|
*/
|
|
26
|
-
/** The default cut point:
|
|
26
|
+
/** 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
|
|
27
27
|
* input under automatic and explicit prompt caching (about 17% less at 128k, 10-14% at 32k). */
|
|
28
28
|
export const DEFAULT_CUT_AT = 0.6;
|
|
29
|
-
export const bandOf = (room: number, cutAt = 1): number => {
|
|
29
|
+
export const bandOf = (room: number, cutAt = 1, adaptive?: { system: number; tokensAfter: number; factor: number }): number => {
|
|
30
30
|
const positive = Math.max(0, room);
|
|
31
|
-
|
|
31
|
+
const base = Math.floor(positive * cutAt);
|
|
32
|
+
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);
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
/** The APIs whose request converter (pi-ai openai-responses-shared) sends each signed thinking block back as an input reasoning item. */
|
|
@@ -100,6 +101,8 @@ export type Decision<D extends object = {}> =
|
|
|
100
101
|
| { kind: "cut"; target: Target; plan: Extract<Plan<D>, { draft: true }> }
|
|
101
102
|
| { kind: "fault"; capacity: boolean; message: string };
|
|
102
103
|
export interface CutInput<D extends object = {}> {
|
|
104
|
+
/** The band adapts by itself (no explicit cut point), so a cut that isn't due must also free a tenth of the room. */
|
|
105
|
+
usefulOnly?: boolean;
|
|
103
106
|
/** Saved conversation, measured by the planner's own function. */
|
|
104
107
|
before: number;
|
|
105
108
|
/** Input that no cut can remove: system part, host input, queued messages, or the incoming prompt. */
|
|
@@ -126,19 +129,22 @@ export type Verdict<D extends object = {}> = Exclude<Decision<D>, { kind: "defer
|
|
|
126
129
|
* 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:
|
|
127
130
|
* 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
|
|
128
131
|
* 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
|
|
129
|
-
* and a cut is due.
|
|
132
|
+
* and a cut is due. An adaptive band can also reach the margin, in which case the room plan is tried first.
|
|
130
133
|
* The margin adds no new decision fault. Exceptions thrown by `plan` are not caught here: they are faults, and they now surface at
|
|
131
134
|
* boundaries inside the band too, where no planner ran before the margin.
|
|
132
135
|
*/
|
|
133
136
|
export function chooseCut<D extends object = {}>(input: CutInput<D> & { opportunistic: boolean }): Verdict<D> {
|
|
134
137
|
const { before, mandatory, room, band, fits, plan, unreduced } = input;
|
|
135
138
|
const lands = (p: Plan<D>): p is Extract<Plan<D>, { draft: true }> => p.draft && p.after < before && fits(p.after + mandatory, band);
|
|
139
|
+
// 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.
|
|
140
|
+
// 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.
|
|
141
|
+
const worthwhile = (p: Extract<Plan<D>, { draft: true }>) => !input.usefulOnly || !input.opportunistic || !fits(10 * (before - p.after), room);
|
|
136
142
|
let roomPlan: Plan<D> | undefined;
|
|
137
143
|
const exact = () => roomPlan ??= plan("room");
|
|
138
|
-
if (band >= bandOf(room)) { const e = exact(); if (lands(e)) return { kind: "cut", target: "room", plan: e }; }
|
|
144
|
+
if (band >= bandOf(room)) { const e = exact(); if (lands(e) && worthwhile(e)) return { kind: "cut", target: "room", plan: e }; }
|
|
139
145
|
const preferred = plan("band");
|
|
140
146
|
if (input.opportunistic) {
|
|
141
|
-
if (!lands(preferred)) return { kind: "none" };
|
|
147
|
+
if (!lands(preferred) || !worthwhile(preferred)) return { kind: "none" };
|
|
142
148
|
const e = exact();
|
|
143
149
|
return e.draft && e.mode === preferred.mode ? { kind: "cut", target: "band", plan: preferred } : { kind: "none" };
|
|
144
150
|
}
|
package/src/build-id.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const BUILD_ID = "
|
|
1
|
+
export const BUILD_ID = "9457180f73cad9d9bde254328dad70dd2abe8400e373a68e06ad455b5b66f203";
|
package/src/compaction.ts
CHANGED
|
@@ -103,9 +103,9 @@ export interface CompactionOptions {
|
|
|
103
103
|
/** Optional estimated complete-input ceiling, separate from physical output reservation.
|
|
104
104
|
* The host owns final serialization/transport counting; this is not an exact wire-token cap. */
|
|
105
105
|
activeInputTokens?: number | ((ctx: ExtensionContext) => number);
|
|
106
|
-
/**
|
|
106
|
+
/** An explicit cut point: a fraction of the input room, above 0 and at most 1. A cut is triggered at, and preferably planned to, the band,
|
|
107
107
|
* 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
|
|
108
|
-
* stays the limit of every throw, fault and the complete-context guard.
|
|
108
|
+
* stays the limit of every throw, fault and the complete-context guard. If omitted, the band adapts to the last cut. */
|
|
109
109
|
cutAt?: number;
|
|
110
110
|
/** The response reserve: the input room is the context window less this. By default the room never exceeds the window less the model's
|
|
111
111
|
* 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
|
|
@@ -216,9 +216,9 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
216
216
|
const learn = (value: number) => { ratios.push(value); if (ratios.length > CALIBRATION_WINDOW) ratios.shift(); factor = Math.max(1, ...ratios); };
|
|
217
217
|
const corrected = (tokens: number, ctx: ExtensionContext) => { identityFor(ctx); return Math.ceil(tokens * factor); };
|
|
218
218
|
// The cut planner's raw reserve for the target limit, on the same factor as every other consumer.
|
|
219
|
-
const planReserve = (target: Target, room: number, mandatory: number, ctx: ExtensionContext) => {
|
|
219
|
+
const planReserve = (target: Target, room: number, band: number, mandatory: number, ctx: ExtensionContext) => {
|
|
220
220
|
identityFor(ctx);
|
|
221
|
-
return planningReserve(ctx.model?.contextWindow ?? 0, target === "band" ?
|
|
221
|
+
return planningReserve(ctx.model?.contextWindow ?? 0, target === "band" ? band : room, factor, mandatory);
|
|
222
222
|
};
|
|
223
223
|
pi.on("message_end", (event, ctx) => {
|
|
224
224
|
if (event.message.role !== "assistant") return;
|
|
@@ -280,10 +280,16 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
280
280
|
throw new Error("Invalid active-input token ceiling");
|
|
281
281
|
return Math.min((ctx.model?.contextWindow ?? 0) - response, ceiling ?? Infinity);
|
|
282
282
|
};
|
|
283
|
-
//
|
|
283
|
+
// Every throw, fault and complete-context guard uses the exact room.
|
|
284
284
|
const limits = (ctx: ExtensionContext) => {
|
|
285
285
|
const { response } = requestReserve(ctx), room = inputRoom(response, ctx);
|
|
286
|
-
return { response, room
|
|
286
|
+
return { response, room };
|
|
287
|
+
};
|
|
288
|
+
const decisionBand = (ctx: ExtensionContext, room: number, system: number) => {
|
|
289
|
+
const last = ctx.sessionManager.getBranch().findLast(entry => entry.type === "compaction");
|
|
290
|
+
const after = last?.type === "compaction" ? (last.details as { tokensAfter?: unknown } | undefined)?.tokensAfter : undefined;
|
|
291
|
+
return bandOf(room, cutAt, options.cutAt === undefined && typeof after === "number" && Number.isFinite(after) && after >= 0
|
|
292
|
+
? { system, tokensAfter: after, factor } : undefined);
|
|
287
293
|
};
|
|
288
294
|
const fits = (tokens: number, limit: number, ctx: ExtensionContext) => corrected(tokens, ctx) < limit;
|
|
289
295
|
// The saved conversation and a cut's quantities are counted by the one function the guard counts with (strict, as the context hook is).
|
|
@@ -413,13 +419,14 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
413
419
|
ctx.ui.notify("pi-tuck does not accept summarization instructions; use /compact without arguments.", "warning");
|
|
414
420
|
return { cancel: true };
|
|
415
421
|
}
|
|
416
|
-
const { response, room
|
|
422
|
+
const { response, room } = limits(ctx);
|
|
417
423
|
const retain = operatorManual || admittedRun;
|
|
418
424
|
// A cut Pi asks for (an operator's /compact, Pi's threshold or overflow recovery) charges what the request must carry whatever is
|
|
419
425
|
// cut: the system part and the host input. A prompt's cut is pi-tuck's own and never comes here.
|
|
420
426
|
const saved = ctx.sessionManager.buildSessionProjection().messages;
|
|
421
427
|
const host = hostTokens(ctx);
|
|
422
|
-
const
|
|
428
|
+
const system = systemPart(ctx.getSystemPrompt(), toolShapes(pi.getActiveTools()), saved, ctx.getSystemPrompt());
|
|
429
|
+
const mandatory = system + host, band = decisionBand(ctx, room, system);
|
|
423
430
|
const before = plannerMeasureFor(ctx)(saved);
|
|
424
431
|
// Pi's own usage threshold (reason "threshold") counts the last reply's output tokens, reasoning included, plus the messages after
|
|
425
432
|
// it: a basis this compactor does not plan on. It may ask for a cut on a request that fits. This compactor cuts only where its own
|
|
@@ -433,11 +440,11 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
433
440
|
}
|
|
434
441
|
// Pi checks old history before appending a new idle prompt. During an active run/manual cut the latest admitted input stays protected.
|
|
435
442
|
const decision = chooseCut({
|
|
436
|
-
before, mandatory, room, band, fits: (tokens, limit) => fits(tokens, limit, ctx),
|
|
443
|
+
before, mandatory, room, band, fits: (tokens, limit) => fits(tokens, limit, ctx), usefulOnly: options.cutAt === undefined,
|
|
437
444
|
opportunistic: thresholdOpportunistic,
|
|
438
445
|
unreduced: "Compaction did not reduce context within the estimated request budget",
|
|
439
446
|
plan: (target): Plan<{ compaction: NonNullable<ReturnType<typeof tuckDraft>["compaction"]>; reserve: number }> => {
|
|
440
|
-
const reserve = planReserve(target, room, mandatory, ctx);
|
|
447
|
+
const reserve = planReserve(target, room, band, mandatory, ctx);
|
|
441
448
|
// Every cut asked while no run is admitted (an operator's cut) is planned for a run that has not begun: the session has moved on
|
|
442
449
|
// from any run that ended. A retry is inside its run.
|
|
443
450
|
const draft = tuckDraft(tuck, event, ctx, reserve, plannerMeasureFor(ctx), options.recallPolicy, retain, operatorManual, operatorManual || !admittedRun);
|
|
@@ -479,10 +486,11 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
479
486
|
access();
|
|
480
487
|
if (!settingsFor().getCompactionSettings(ctx.model).enabled) return;
|
|
481
488
|
if (!ctx.model) throw missingModel();
|
|
482
|
-
const { response, room
|
|
489
|
+
const { response, room } = limits(ctx);
|
|
483
490
|
// Public early view only: later handlers, image normalization and tool-loop growth remain the complete-context guard's responsibility.
|
|
484
491
|
const saved = ctx.sessionManager.buildSessionProjection().messages;
|
|
485
492
|
const system = systemPart(observedPrompt, toolShapes(tools), saved, ctx.getSystemPrompt());
|
|
493
|
+
const band = decisionBand(ctx, room, system);
|
|
486
494
|
const host = hostTokens(ctx), measure = measureFor(ctx), before = measure(saved);
|
|
487
495
|
const incoming = incomingPart + system, mandatory = incoming + host;
|
|
488
496
|
if (!fits(incoming, room, ctx))
|
|
@@ -493,10 +501,10 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
493
501
|
if (gate.kind !== "plan") return;
|
|
494
502
|
const previousSummary = saved.find(message => message.role === "compactionSummary")?.summary;
|
|
495
503
|
const decision = chooseCut({
|
|
496
|
-
before, mandatory, room, band, fits: (tokens, limit) => fits(tokens, limit, ctx), opportunistic: gate.opportunistic,
|
|
504
|
+
before, mandatory, room, band, fits: (tokens, limit) => fits(tokens, limit, ctx), usefulOnly: options.cutAt === undefined, opportunistic: gate.opportunistic,
|
|
497
505
|
unreduced: "Incoming prompt compaction did not reduce context within the estimated request budget",
|
|
498
506
|
plan: (target): Plan<{ compaction: NonNullable<ReturnType<typeof tuckObservedDraft>["compaction"]>; reserve: number }> => {
|
|
499
|
-
const reserve = planReserve(target, room, mandatory, ctx);
|
|
507
|
+
const reserve = planReserve(target, room, band, mandatory, ctx);
|
|
500
508
|
// Only a due cut re-renders a summary that sits alone in the session: inside the band a cut is opportunistic, and a smaller summary
|
|
501
509
|
// there buys nothing the request needed and costs history the next recall would have found.
|
|
502
510
|
const draft = tuckObservedDraft(tuck, { branchEntries: ctx.sessionManager.getBranch(), tokensBefore: before, previousSummary, newPrompt: true, reRenderSummary: !gate.opportunistic }, ctx, reserve, plannerMeasureFor(ctx), options.recallPolicy, false);
|
|
@@ -532,12 +540,13 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
532
540
|
if (event.entries.length || !isDeepStrictEqual(event.context.contextMessages,
|
|
533
541
|
ctx.sessionManager.buildSessionProjection().messages)) return;
|
|
534
542
|
if (!ctx.model) return;
|
|
535
|
-
const { response, room
|
|
543
|
+
const { response, room } = limits(ctx);
|
|
536
544
|
const measure = measureFor(ctx), saved = event.context.contextMessages;
|
|
537
545
|
// One snapshot per decision. Messages queued for the next request (steering, follow-ups) are in that request but not in the
|
|
538
546
|
// saved conversation. Pi's peek returns the steering queue, else the follow-up queue; a follow-up is not in the very next
|
|
539
547
|
// request while tools still run, so it can be counted early: conservative, and nothing else would cut at that later boundary.
|
|
540
548
|
const system = systemPart(ctx.getSystemPrompt(), toolShapes(pi.getActiveTools()), saved, ctx.getSystemPrompt());
|
|
549
|
+
const band = decisionBand(ctx, room, system);
|
|
541
550
|
const host = hostTokens(ctx);
|
|
542
551
|
const queued = event.context.pendingMessages.length ? conversationTokens(event.context.pendingMessages as Messages) : 0;
|
|
543
552
|
const mandatory = system + host + queued;
|
|
@@ -551,10 +560,10 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
551
560
|
floorKept = undefined;
|
|
552
561
|
const before = measure(saved);
|
|
553
562
|
return decideCut({
|
|
554
|
-
total: before + mandatory, mandatory, before, room, band, fits: (tokens, limit) => fits(tokens, limit, ctx),
|
|
563
|
+
total: before + mandatory, mandatory, before, room, band, fits: (tokens, limit) => fits(tokens, limit, ctx), usefulOnly: options.cutAt === undefined,
|
|
555
564
|
unreduced: "Tool-loop preflight compaction did not reduce context within the estimated request budget",
|
|
556
565
|
plan: (target): Plan<{ compaction: NonNullable<ReturnType<typeof tuckObservedDraft>["compaction"]>; reserve: number }> => {
|
|
557
|
-
const reserve = planReserve(target, room, mandatory, ctx);
|
|
566
|
+
const reserve = planReserve(target, room, band, mandatory, ctx);
|
|
558
567
|
const draft = tuckObservedDraft(tuck, { branchEntries: ctx.sessionManager.getBranch(), tokensBefore: before, previousSummary }, ctx, reserve, plannerMeasureFor(ctx), options.recallPolicy, true);
|
|
559
568
|
if (!draft.compaction) {
|
|
560
569
|
// The planner's own floor, so a clip's second budget is measured against what the smallest cut really keeps.
|
package/src/omissions.ts
CHANGED
|
@@ -5,12 +5,12 @@ import { createRecall, type RecallPolicy } from './recall';
|
|
|
5
5
|
/** Native branch edits supply exclusion; original global indices supply provenance. */
|
|
6
6
|
export function omittedSourceIndices(entries: readonly SessionEntry[], branch: readonly SessionEntry[]): ReadonlySet<number> {
|
|
7
7
|
const indices = buildGlobalIndexById(entries);
|
|
8
|
-
const edits = new
|
|
8
|
+
const edits = new Set<string>();
|
|
9
9
|
for (const entry of branch) if (entry.type === 'context_edit') {
|
|
10
10
|
if (entry.replacement !== null) throw new Error('pi-tuck does not support replacement context edits');
|
|
11
|
-
edits.
|
|
11
|
+
edits.add(entry.targetId);
|
|
12
12
|
}
|
|
13
|
-
return new Set([...edits].flatMap(
|
|
13
|
+
return new Set([...edits].flatMap(id => indices.has(id) ? [indices.get(id)!] : []));
|
|
14
14
|
}
|
|
15
15
|
/** Preserve surviving record bytes/order, and the summary's format (a v1 summary stays v1). No text matching or semantic inference. */
|
|
16
16
|
export function filterOmittedSummary(summary: string, omitted: ReadonlySet<number>): string {
|
|
@@ -49,22 +49,20 @@ export function filterCustomQuotes(summary: string, entries: readonly SessionEnt
|
|
|
49
49
|
}
|
|
50
50
|
type ProjectedMessages = ReturnType<ExtensionContext['sessionManager']['buildSessionProjection']>['messages'];
|
|
51
51
|
/** The saved conversation as this compactor delivers it: native omissions and ineligible custom quotes removed from summaries. */
|
|
52
|
-
|
|
52
|
+
function projectMessages(messages: ProjectedMessages, ctx: ExtensionContext, policy: RecallPolicy | undefined, allowForeign: boolean): ProjectedMessages {
|
|
53
53
|
const entries = ctx.sessionManager.getEntries(), branch = ctx.sessionManager.getBranch();
|
|
54
54
|
const omitted = omittedSourceIndices(entries, branch);
|
|
55
|
-
return messages.map(message => message.role === 'compactionSummary'
|
|
55
|
+
return messages.map(message => message.role === 'compactionSummary' && (!allowForeign || splitSummary(message.summary) !== undefined)
|
|
56
56
|
? { ...message, summary: filterCustomQuotes(filterOmittedSummary(message.summary, omitted), entries, branch, policy) } : message);
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
export function projectedMessages(messages: ProjectedMessages, ctx: ExtensionContext, policy?: RecallPolicy): ProjectedMessages {
|
|
59
|
+
return projectMessages(messages, ctx, policy, false);
|
|
60
|
+
}
|
|
60
61
|
/**
|
|
61
62
|
* The same as `projectedMessages` for every summary in this compactor's format, and it never throws for any other: a summary of another
|
|
62
63
|
* format (a Pi default summary, an empty draft) cannot be filtered and is left as it is. For counting a conversation that a cut is about
|
|
63
64
|
* to replace, such as an operator's /compact on a session whose saved summary is foreign. The context hook and the triggers stay strict.
|
|
64
65
|
*/
|
|
65
66
|
export function measurableMessages(messages: ProjectedMessages, ctx: ExtensionContext, policy?: RecallPolicy): ProjectedMessages {
|
|
66
|
-
|
|
67
|
-
const omitted = omittedSourceIndices(entries, branch);
|
|
68
|
-
return messages.map(message => message.role === 'compactionSummary' && isTranscriptSummary(message.summary)
|
|
69
|
-
? { ...message, summary: filterCustomQuotes(filterOmittedSummary(message.summary, omitted), entries, branch, policy) } : message);
|
|
67
|
+
return projectMessages(messages, ctx, policy, true);
|
|
70
68
|
}
|
package/src/semantic-recall.ts
CHANGED
|
@@ -21,12 +21,11 @@ function physicalPath(path:string):string {
|
|
|
21
21
|
const next=dirname(parent);if(next===parent)throw error;parts.unshift(basename(parent));parent=next;
|
|
22
22
|
}}
|
|
23
23
|
}
|
|
24
|
+
const inside=(base:string,target:string)=>{const rel=relative(base,target);return rel!=='..'&&!rel.startsWith('..'+sep)&&!isAbsolute(rel);};
|
|
24
25
|
function outsideProject(project:string,path:string,kind:'configuration'|'index'|'runtime'|'scratch'){
|
|
25
|
-
const inside=(base:string,target:string)=>{const rel=relative(base,target);return rel!=='..'&&!rel.startsWith('..'+sep)&&!isAbsolute(rel);};
|
|
26
26
|
if(inside(resolve(project),resolve(path))||inside(physicalPath(project),physicalPath(path)))throw new Error('Semantic '+kind+' must be outside project');
|
|
27
27
|
}
|
|
28
28
|
function outsideRuntime(runtime:string,path:string,kind:'index'|'scratch'){
|
|
29
|
-
const inside=(base:string,target:string)=>{const rel=relative(base,target);return rel!=='..'&&!rel.startsWith('..'+sep)&&!isAbsolute(rel);};
|
|
30
29
|
if(inside(resolve(runtime),resolve(path))||inside(physicalPath(runtime),physicalPath(path)))throw new Error('Semantic '+kind+' must be outside immutable runtime');
|
|
31
30
|
}
|
|
32
31
|
function scratchParent(config:SemanticRecallConfig){return config.scratchDirectory??dirname(config.indexDirectory);}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session-global message indices — the single definition of the `#N` index space.
|
|
3
3
|
*
|
|
4
|
-
* Recall
|
|
4
|
+
* Recall numbers every `type == "message"` entry
|
|
5
5
|
* in session-file order, counting across compaction windows and abandoned
|
|
6
6
|
* branches. Compaction summaries must emit the same numbers, but `normalize`
|
|
7
7
|
* historically numbered the selected window from zero. This module owns the
|
|
@@ -15,41 +15,27 @@ export const isCountedMessageEntry = (entry: any): boolean =>
|
|
|
15
15
|
entry?.type === "message" && entry.message != null;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
* in
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* produce no map entry. Duplicate ids are ambiguous — dropped fail-closed so
|
|
23
|
-
* callers emit no ref instead of a wrong one.
|
|
18
|
+
* Map session entry ids to their global message index (position among counted
|
|
19
|
+
* message entries in array order, which matches session-file order). Entries
|
|
20
|
+
* without a usable id still occupy an index. Duplicate ids are dropped
|
|
21
|
+
* fail-closed so callers emit no ref instead of a wrong one.
|
|
24
22
|
*/
|
|
25
|
-
const
|
|
23
|
+
export const buildGlobalIndexById = (entries: readonly any[]): Map<string, number> => {
|
|
26
24
|
const byId = new Map<string, number>();
|
|
27
25
|
const ambiguous = new Set<string>();
|
|
28
26
|
let messageIndex = 0;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
byId.set(id, messageIndex);
|
|
39
|
-
}
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!isCountedMessageEntry(entry)) continue;
|
|
29
|
+
const id = entry?.id;
|
|
30
|
+
if (typeof id === "string" && id.length > 0) {
|
|
31
|
+
if (byId.has(id) || ambiguous.has(id)) {
|
|
32
|
+
byId.delete(id);
|
|
33
|
+
ambiguous.add(id);
|
|
34
|
+
} else {
|
|
35
|
+
byId.set(id, messageIndex);
|
|
40
36
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Map session entry ids to their global message index (position among counted
|
|
49
|
-
* message entries in array order, which matches session-file order).
|
|
50
|
-
*/
|
|
51
|
-
export const buildGlobalIndexById = (entries: readonly any[]): Map<string, number> => {
|
|
52
|
-
const b = createGlobalIndexBuilder();
|
|
53
|
-
for (const entry of entries) b.add(entry);
|
|
54
|
-
return b.map();
|
|
37
|
+
}
|
|
38
|
+
messageIndex++;
|
|
39
|
+
}
|
|
40
|
+
return byId;
|
|
55
41
|
};
|
|
@@ -54,7 +54,7 @@ const normalizeOne = (msg: CompilerMessage, msgIndex: number | undefined): Norma
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Normalize messages to blocks. `sourceIndices`, when provided, supplies the
|
|
57
|
-
* session-global `#N` index per input position (see
|
|
57
|
+
* session-global `#N` index per input position (see global-indices.ts).
|
|
58
58
|
* A missing entry yields `sourceIndex: undefined`, which downstream renderers
|
|
59
59
|
* display as no ref — never a window-relative number. Omitted entirely, the
|
|
60
60
|
* legacy positional behavior is preserved (used by callers/tests that have no
|
|
@@ -71,23 +71,21 @@ export const normalize = (
|
|
|
71
71
|
if (sourceEntryIds && sourceEntryIds.length !== messages.length) throw new Error("Original entry IDs must align with compiler messages");
|
|
72
72
|
if (sourceTimes && sourceTimes.length !== messages.length) throw new Error("Original times must align with compiler messages");
|
|
73
73
|
const timeByIndex = Object.fromEntries((sourceIndices ?? messages.map((_, i) => i)).flatMap((index, i) => index === undefined || !sourceTimes ? [] : [[index, sourceTimes[i]]]));
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
receipts: { name: string; order: number; index: number | undefined; isError: boolean; content: Extract<CompilerMessage, { role: "toolResult" }>["content"] }[] }>();
|
|
74
|
+
const pairs = new Map<string, { calls: { name: string; order: number; args: Record<string, unknown> }[];
|
|
75
|
+
receipts: { name: string; order: number }[] }>();
|
|
77
76
|
const group = (id: string) => {
|
|
78
77
|
if (typeof id !== "string" || !id) throw new Error("Tool history lacks a recorded call identity");
|
|
79
78
|
let pair = pairs.get(id);
|
|
80
79
|
if (!pair) { pair = { calls: [], receipts: [] }; pairs.set(id, pair); }
|
|
81
80
|
return pair;
|
|
82
81
|
};
|
|
83
|
-
|
|
82
|
+
(receiptContext?.messages ?? messages).forEach((msg, order) => {
|
|
84
83
|
if (msg.role === "assistant" && Array.isArray(msg.content)) for (const part of msg.content) {
|
|
85
|
-
if (part.type === "toolCall") group(part.id).calls.push({ name: part.name, order,
|
|
84
|
+
if (part.type === "toolCall") group(part.id).calls.push({ name: part.name, order, args: part.arguments });
|
|
86
85
|
}
|
|
87
86
|
if (msg.role === "toolResult") {
|
|
88
87
|
if (typeof msg.isError !== "boolean") throw new Error("Tool receipt lacks a recorded error status");
|
|
89
|
-
group(msg.toolCallId).receipts.push({ name: msg.toolName,
|
|
90
|
-
order, index: context.sourceIndices[order], isError: msg.isError, content: msg.content });
|
|
88
|
+
group(msg.toolCallId).receipts.push({ name: msg.toolName, order });
|
|
91
89
|
}
|
|
92
90
|
});
|
|
93
91
|
// A call paired with exactly one later receipt of the same tool has a recorded outcome. An export through recall keeps its outcome block.
|
package/src/tuck/cut.ts
CHANGED
|
@@ -55,42 +55,18 @@ export type OwnCutResult =
|
|
|
55
55
|
| { ok: false; reason: OwnCutCancelReason };
|
|
56
56
|
|
|
57
57
|
const collectLiveMessages = (branchEntries: any[]): EntryWithMessage[] => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
lastKeptId = branchEntries[i].firstKeptEntryId;
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Orphan recovery: triggers when lastKeptId is set to "" (sentinel from prior
|
|
70
|
-
// compact-all) OR set to an id that no longer exists in the branch. In both cases,
|
|
71
|
-
// start collecting from right after the last compaction entry.
|
|
72
|
-
const hasPriorCompaction = lastCompactionIdx >= 0;
|
|
73
|
-
const hasValidKeptId = !!lastKeptId && branchEntries.some((e: any) => e.id === lastKeptId);
|
|
74
|
-
const orphanRecovery = hasPriorCompaction && !hasValidKeptId;
|
|
75
|
-
|
|
76
|
-
// Collect live messages
|
|
58
|
+
const lastCompactionIdx = branchEntries.findLastIndex(e => e.type === "compaction");
|
|
59
|
+
const lastKeptId = branchEntries[lastCompactionIdx]?.firstKeptEntryId;
|
|
60
|
+
const kept = lastKeptId ? branchEntries.findIndex(e => e.id === lastKeptId) : -1;
|
|
61
|
+
// An empty compact-all sentinel or a missing anchor resumes after the last
|
|
62
|
+
// compaction. With no prior compaction, collection starts at the beginning.
|
|
63
|
+
const start = kept >= 0 ? kept : lastCompactionIdx + 1;
|
|
77
64
|
const liveMessages: EntryWithMessage[] = [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (m) liveMessages.push({ entry: e, message: m });
|
|
84
|
-
}
|
|
85
|
-
} else {
|
|
86
|
-
let foundKept = !lastKeptId; // if no prior compaction, start collecting immediately
|
|
87
|
-
for (const e of branchEntries) {
|
|
88
|
-
if (!foundKept && e.id === lastKeptId) foundKept = true;
|
|
89
|
-
if (!foundKept) continue;
|
|
90
|
-
if (e.type === "compaction") continue;
|
|
91
|
-
const m = toLiveMessage(e);
|
|
92
|
-
if (m) liveMessages.push({ entry: e, message: m });
|
|
93
|
-
}
|
|
65
|
+
for (let i = start; i < branchEntries.length; i++) {
|
|
66
|
+
const e = branchEntries[i];
|
|
67
|
+
if (e.type === "compaction") continue;
|
|
68
|
+
const m = toLiveMessage(e);
|
|
69
|
+
if (m) liveMessages.push({ entry: e, message: m });
|
|
94
70
|
}
|
|
95
71
|
return liveMessages;
|
|
96
72
|
};
|