pi-tuck 1.0.1 → 1.0.4

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 (50) hide show
  1. package/CHANGELOG.md +65 -3
  2. package/README.md +138 -57
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.js +7 -4
  5. package/dist/src/budget.d.ts +17 -4
  6. package/dist/src/budget.js +33 -10
  7. package/dist/src/build-id.d.ts +1 -1
  8. package/dist/src/build-id.js +1 -1
  9. package/dist/src/compaction.d.ts +7 -0
  10. package/dist/src/compaction.js +32 -16
  11. package/dist/src/diagnostics.d.ts +2 -1
  12. package/dist/src/omissions.d.ts +1 -1
  13. package/dist/src/omissions.js +8 -8
  14. package/dist/src/tuck/core/brief.d.ts +13 -43
  15. package/dist/src/tuck/core/brief.js +41 -255
  16. package/dist/src/tuck/core/filter-noise.js +2 -1
  17. package/dist/src/tuck/core/format.d.ts +2 -6
  18. package/dist/src/tuck/core/format.js +4 -10
  19. package/dist/src/tuck/core/normalize.js +22 -50
  20. package/dist/src/tuck/core/shrink.d.ts +33 -0
  21. package/dist/src/tuck/core/shrink.js +218 -0
  22. package/dist/src/tuck/core/summarize.d.ts +15 -7
  23. package/dist/src/tuck/core/summarize.js +17 -22
  24. package/dist/src/tuck/core/transcript.d.ts +24 -13
  25. package/dist/src/tuck/core/transcript.js +251 -83
  26. package/dist/src/tuck/types.d.ts +4 -21
  27. package/dist/src/tuck.d.ts +8 -10
  28. package/dist/src/tuck.js +23 -25
  29. package/index.ts +8 -5
  30. package/package.json +1 -1
  31. package/src/budget.ts +29 -9
  32. package/src/build-id.ts +1 -1
  33. package/src/compaction.ts +33 -14
  34. package/src/diagnostics.ts +1 -1
  35. package/src/omissions.ts +8 -8
  36. package/src/tuck/core/brief.ts +48 -265
  37. package/src/tuck/core/filter-noise.ts +2 -1
  38. package/src/tuck/core/format.ts +4 -11
  39. package/src/tuck/core/normalize.ts +21 -50
  40. package/src/tuck/core/shrink.ts +217 -0
  41. package/src/tuck/core/summarize.ts +26 -24
  42. package/src/tuck/core/transcript.ts +238 -82
  43. package/src/tuck/types.ts +9 -20
  44. package/src/tuck.ts +30 -32
  45. package/dist/src/tuck/core/brief-meter.d.ts +0 -42
  46. package/dist/src/tuck/core/brief-meter.js +0 -273
  47. package/dist/src/tuck/core/rank.d.ts +0 -42
  48. package/dist/src/tuck/core/rank.js +0 -292
  49. package/src/tuck/core/brief-meter.ts +0 -235
  50. package/src/tuck/core/rank.ts +0 -331
@@ -15,21 +15,21 @@ export const smallOriginalArguments = (value, maxCharacters = MAX_PRODUCER_ARGUM
15
15
  return false;
16
16
  }
17
17
  };
18
- export const TRANSCRIPT_HEADER = "TUCK-TRANSCRIPT/1";
19
- export const TRANSCRIPT_UNIT_CHARACTERS = 120;
20
- /** UTF-16 serialized characters, including JSON escapes, labels and framing. */
21
- export const lineUnits = (characters) => Math.max(1, Math.ceil(characters / TRANSCRIPT_UNIT_CHARACTERS));
22
- export const transcriptUnits = (text) => text === "" ? 0 :
23
- text.split("\n").reduce((n, line) => n + lineUnits(line.length), 0);
24
- export const serializeRecords = (records) => records.map(r => JSON.stringify(r)).join("\n");
25
- export const serializeTranscript = (records) => records.length ?
26
- `${TRANSCRIPT_HEADER}\n${serializeRecords(records)}` : "";
18
+ /** The format this build writes. */
19
+ export const TRANSCRIPT_HEADER = "TUCK-TRANSCRIPT/2";
20
+ /** The format of 1.0.x summaries: one JSON object per line. Read, never written. */
21
+ export const TRANSCRIPT_HEADER_V1 = "TUCK-TRANSCRIPT/1";
22
+ /** The header of summaries saved before the rename to pi-tuck: the v1 format. */
23
+ export const LEGACY_TRANSCRIPT_HEADER = "VCC-TRANSCRIPT/1";
24
+ /** The header a summary of `version` is written with; a legacy one is rewritten as v1. */
25
+ export const transcriptHeader = (version) => version === 2 ? TRANSCRIPT_HEADER : TRANSCRIPT_HEADER_V1;
27
26
  /** Starts a summary's closing section, after the record lines. */
28
27
  export const CLOSING_SEPARATOR = "\n\n---\n\n";
29
- /** A summary in this format, split by structure: each record is one JSON line, so the first blank line after the header starts the
30
- * closing section. Any closing wording, from this build or an earlier one, is accepted; the records are returned byte for byte. */
28
+ /** A summary in either format, split by structure: each record is one line, and no record line is empty, so the first blank line after the
29
+ * header starts the closing section. Any closing wording, from this build or an earlier one, is accepted; the records are returned byte
30
+ * for byte, with the format their header names. */
31
31
  export const splitSummary = (summary) => {
32
- const header = [TRANSCRIPT_HEADER, LEGACY_TRANSCRIPT_HEADER].find(h => summary.startsWith(h + "\n"));
32
+ const header = [TRANSCRIPT_HEADER, TRANSCRIPT_HEADER_V1, LEGACY_TRANSCRIPT_HEADER].find(h => summary.startsWith(h + "\n"));
33
33
  if (!header)
34
34
  return undefined;
35
35
  const at = summary.indexOf(CLOSING_SEPARATOR, header.length);
@@ -37,79 +37,247 @@ export const splitSummary = (summary) => {
37
37
  return undefined;
38
38
  // A summary saved before the rename keeps its records byte for byte (their lengths are checked); only its closing is pointed at the
39
39
  // tools the model has now.
40
- const closing = header === TRANSCRIPT_HEADER ? summary.slice(at) : summary.slice(at).replaceAll("vcc_recall", "tuck_recall").replaceAll("vcc_search", "tuck_search");
41
- return { body: summary.slice(header.length + 1, at), closing };
40
+ const closing = header === LEGACY_TRANSCRIPT_HEADER ? summary.slice(at).replaceAll("vcc_recall", "tuck_recall").replaceAll("vcc_search", "tuck_search") : summary.slice(at);
41
+ return { body: summary.slice(header.length + 1, at), closing, version: header === TRANSCRIPT_HEADER ? 2 : 1 };
42
42
  };
43
- /** The header of summaries saved before the rename to pi-tuck. */
44
- export const LEGACY_TRANSCRIPT_HEADER = "VCC-TRANSCRIPT/1";
45
43
  const LEGACY_RECEIPT_RANGE_NOTICE = RECEIPT_RANGE_NOTICE.replace("tuck_recall", "vcc_recall");
46
- /** Accept our explicit record protocol only. Never recover roles from payload text. */
47
- export const parseRecords = (text) => {
44
+ /*
45
+ * TUCK-TRANSCRIPT/2: the header line, one line per record, then the closing section.
46
+ *
47
+ * line = head ":" [ " " text ] no " " and no text when the text is empty
48
+ * record = [ locator " " ] role [ " " id ] [ " " time ] [ " " span ] ":" ... excerpt
49
+ * | [ locator " " ] [ role [ " " id ] " " ] "omitted" [ " " time ] [ " " span ] ":" ... omission of part of one source,
50
+ * or, with no role, of the tool history
51
+ * | role " omitted " count [ " " locator-time { " " locator-time } ] ":" ... omission of unselected text blocks
52
+ * | [ locator " " ] title ":" ... section (file ledger, action trail)
53
+ * locator = "#" index the source's session-wide index, as tuck_recall {"index"} takes it
54
+ * locator-time = locator [ "@" time-value ] every locator of a record has a time, or none does
55
+ * role = "user" | "assistant" | "custom"
56
+ * id = a JSON string the native entry id of a custom notice (role "custom" only)
57
+ * time = "@" time-value the recorded time of the locator that precedes the role (a record that has one)
58
+ * time-value = UTC | "journal:" UTC | "unknown" UTC is the message time, "journal:" UTC the session file's time of the entry
59
+ * span = "[chars " start "-" end " of " total "]" the characters of the source text block the text covers or the omission leaves out
60
+ * count = index how many text blocks the omission counts
61
+ * title = capital letter, then letters, digits, "_" and "-"
62
+ * index = "0" | nonzero digit, then digits a safe integer, no sign, no leading zero
63
+ *
64
+ * The text of an excerpt or section, or the reason of an omission, is the rest of the line: "\" is written "\\", a line feed "\n" and a
65
+ * carriage return "\r"; every other character is written as it is.
66
+ *
67
+ * The head is made of the tokens above only, and the text is the rest of the line after the first ":" that follows them, so no text can be
68
+ * mistaken for structure: a text that looks like a record is the text of one line. A role is lowercase and a title is capitalized, so the
69
+ * first word of a head tells which record it is. Lines that follow no production are rejected, as are fields the record shape does not
70
+ * have (a time without a locator, an id on a role other than "custom") and every check parseRecords makes of a v1 record.
71
+ *
72
+ * Examples:
73
+ * #12 user @2026-10-30T14:00:00Z: Please fix the parser.
74
+ * #12 assistant [chars 0-523 of 2004]: I will start with the tokenizer.
75
+ * #12 assistant omitted [chars 523-1900 of 2004]: tuck_recall {"index":12,"offset":523}
76
+ * #7 custom "a1b2c3d4": A note.
77
+ * assistant omitted 40 #3 #5 #9: Completely unselected historical text blocks; ...
78
+ * user omitted 3 #1@2026-10-30T14:00:00Z #4@unknown #8@journal:2026-10-30T14:00:05Z: Completely unselected ...
79
+ * omitted: Historical tool actions and observations omitted; read original sources with tuck_recall
80
+ * #4 Files: - read src/a.ts (tuck_recall {"index":4})
81
+ */
82
+ const escapeText = (text) => text.replace(/[\\\n\r]/g, c => c === "\\" ? "\\\\" : c === "\n" ? "\\n" : "\\r");
83
+ /** The length of `text` as a record line carries it: one character more for each "\", line feed and carriage return. */
84
+ export const escapedLength = (text) => {
85
+ let length = text.length;
86
+ for (let i = 0; i < text.length; i++) {
87
+ const c = text.charCodeAt(i);
88
+ if (c === 92 || c === 10 || c === 13)
89
+ length++;
90
+ }
91
+ return length;
92
+ };
93
+ const TITLE = /^[A-Z][A-Za-z0-9_-]*$/;
94
+ const refuse = (record) => { throw new Error(`Cannot write a ${record.kind} record as ${TRANSCRIPT_HEADER}`); };
95
+ /** One record as a line of TUCK-TRANSCRIPT/2 (without its newline). The records this build emits only: a record whose fields the grammar
96
+ * cannot carry exactly is refused, never written lossily. */
97
+ export const serializeRecord = (record) => {
98
+ const fields = record;
99
+ const present = Object.keys(fields).filter(key => fields[key] !== undefined);
100
+ const only = (...allowed) => { if (!present.every(key => allowed.includes(key)))
101
+ refuse(record); };
102
+ const index = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0 ? String(n) : refuse(record);
103
+ const time = (t) => !validSourceTime(t) ? refuse(record) : t.basis === "unknown" ? "unknown" : t.basis === "journal" ? `journal:${t.utc}` : t.utc;
104
+ /** The record's times, one for each locator in order; none when it has none. */
105
+ const times = (locators) => {
106
+ const { sourceTimes } = record;
107
+ if (!sourceTimes)
108
+ return undefined;
109
+ if (sourceTimes.length !== locators.length)
110
+ refuse(record);
111
+ return sourceTimes.map(({ sourceIndex, ...value }, i) => sourceIndex === locators[i] ? time(value) : refuse(record));
112
+ };
113
+ const line = (head, text) => head + ":" + (text ? " " + escapeText(text) : "");
114
+ if (record.kind === "section") {
115
+ only("kind", "title", "text", "sourceIndex");
116
+ if (typeof record.title !== "string" || !TITLE.test(record.title) || typeof record.text !== "string")
117
+ refuse(record);
118
+ return line((record.sourceIndex === undefined ? "" : `#${index(record.sourceIndex)} `) + record.title, record.text);
119
+ }
120
+ if (record.kind !== "excerpt" && record.kind !== "omission")
121
+ return refuse(record);
122
+ const { role, sourceIndex, sourceEntryId, span } = record, omission = record.kind === "omission";
123
+ if (role !== undefined && !["user", "assistant", "custom"].includes(role) || role === undefined && (!omission || sourceEntryId !== undefined) ||
124
+ sourceEntryId !== undefined && (role !== "custom" || typeof sourceEntryId !== "string"))
125
+ refuse(record);
126
+ const who = (sourceEntryId === undefined ? "" : ` ${JSON.stringify(sourceEntryId)}`);
127
+ if (omission && record.count !== undefined) {
128
+ only("kind", "role", "count", "sourceIndices", "reason", "sourceTimes");
129
+ const locators = record.sourceIndices ?? [], stamped = times(locators);
130
+ if (role === undefined || typeof record.reason !== "string")
131
+ refuse(record);
132
+ return line(`${role} omitted ${index(record.count)}` + locators.map((n, i) => ` #${index(n)}${stamped ? "@" + stamped[i] : ""}`).join(""), record.reason);
133
+ }
134
+ only("kind", "role", "sourceIndex", omission ? "reason" : "text", "span", "sourceEntryId", "sourceTimes");
135
+ const text = omission ? record.reason : record.text;
136
+ if (typeof text !== "string")
137
+ refuse(record);
138
+ const stamped = times(sourceIndex === undefined ? [] : [sourceIndex]);
139
+ const where = (sourceIndex === undefined ? "" : `#${index(sourceIndex)} `) + (role === undefined ? "" : role + who + (omission ? " " : ""));
140
+ return line(where + (omission ? "omitted" : "") + (stamped ? ` @${stamped[0]}` : "") +
141
+ (span ? ` [chars ${index(span.start)}-${index(span.end)} of ${index(span.totalCharacters)}]` : ""), text);
142
+ };
143
+ export const serializeRecords = (records) => records.map(serializeRecord).join("\n");
144
+ export const serializeTranscript = (records) => records.length ?
145
+ `${TRANSCRIPT_HEADER}\n${serializeRecords(records)}` : "";
146
+ const invalid = () => { throw new Error("Invalid pi-tuck transcript record"); };
147
+ const unescapeText = (text) => text.replace(/\\([\\nr])?/g, (_, c) => c === undefined ? invalid() : c === "n" ? "\n" : c === "r" ? "\r" : "\\");
148
+ // Sticky patterns: each is matched at the position the line has been read up to.
149
+ const INDEX = "(0|[1-9]\\d*)";
150
+ const TIME = "(unknown|(?:journal:)?\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?Z)";
151
+ const LOCATOR = new RegExp(`#${INDEX} `, "y"), ROLE = /user|assistant|custom/y, OMITTED = /omitted/y;
152
+ const ID = / ("(?:[^"\\\u0000-\u001f]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*")/y, SPACE_OMITTED = / omitted/y, COUNT = new RegExp(` ${INDEX}`, "y");
153
+ const LOCATOR_TIME = new RegExp(` #${INDEX}(?:@${TIME})?`, "y"), TIME_TOKEN = new RegExp(` @${TIME}`, "y");
154
+ const SPAN = new RegExp(` \\[chars ${INDEX}-${INDEX} of ${INDEX}\\]`, "y"), TITLE_TOKEN = /[A-Z][A-Za-z0-9_-]*/y;
155
+ /** One line of TUCK-TRANSCRIPT/2, as the record the build that wrote it held (its fields in the order that build emits them). */
156
+ const parseLine = (line) => {
157
+ let at = 0;
158
+ const take = (pattern) => {
159
+ pattern.lastIndex = at;
160
+ const match = pattern.exec(line);
161
+ if (match)
162
+ at = pattern.lastIndex;
163
+ return match ?? undefined;
164
+ };
165
+ const integer = (digits) => Number.isSafeInteger(Number(digits)) ? Number(digits) : invalid();
166
+ const timeOf = (value) => value === "unknown" ? { basis: "unknown" } :
167
+ value.startsWith("journal:") ? { basis: "journal", utc: value.slice(8) } : { basis: "message", utc: value };
168
+ const located = take(LOCATOR), sourceIndex = located ? integer(located[1]) : undefined;
169
+ const title = take(TITLE_TOKEN)?.[0], role = title === undefined ? take(ROLE)?.[0] : undefined;
170
+ const id = role === "custom" ? take(ID)?.[1] : undefined;
171
+ const omitted = title === undefined && take(role === undefined ? OMITTED : SPACE_OMITTED) !== undefined;
172
+ if (title === undefined && role === undefined && !omitted)
173
+ invalid();
174
+ const common = { ...(sourceIndex !== undefined ? { sourceIndex } : {}) };
175
+ let record, text;
176
+ const end = () => {
177
+ if (line[at++] !== ":")
178
+ return invalid();
179
+ const rest = line.slice(at);
180
+ return rest === "" ? "" : rest[0] === " " && rest.length > 1 && !rest.includes("\r") ? unescapeText(rest.slice(1)) : invalid();
181
+ };
182
+ const count = omitted && role !== undefined && sourceIndex === undefined && id === undefined ? take(COUNT) : undefined;
183
+ if (title !== undefined) {
184
+ text = end();
185
+ record = { kind: "section", title, text, ...common };
186
+ }
187
+ else if (count) {
188
+ const items = [];
189
+ for (let item; (item = take(LOCATOR_TIME));)
190
+ items.push({ sourceIndex: integer(item[1]), ...(item[2] !== undefined ? { time: timeOf(item[2]) } : {}) });
191
+ const stamped = items.filter(item => item.time).length;
192
+ if (stamped && stamped !== items.length)
193
+ invalid();
194
+ record = { kind: "omission", role, count: integer(count[1]), ...(items.length ? { sourceIndices: items.map(item => item.sourceIndex) } : {}), reason: end(),
195
+ ...(stamped ? { sourceTimes: items.map(({ sourceIndex: n, time }) => ({ sourceIndex: n, ...time })) } : {}) };
196
+ }
197
+ else {
198
+ const time = take(TIME_TOKEN), span = take(SPAN);
199
+ if (time && sourceIndex === undefined)
200
+ invalid();
201
+ const fields = {
202
+ ...(span ? { span: { coordinateSpace: "sanitized-block", start: integer(span[1]), end: integer(span[2]), totalCharacters: integer(span[3]) } } : {}),
203
+ ...(id !== undefined ? { sourceEntryId: JSON.parse(id) } : {}),
204
+ ...(time ? { sourceTimes: [{ sourceIndex: sourceIndex, ...timeOf(time[1]) }] } : {}),
205
+ };
206
+ text = end();
207
+ record = omitted ? { kind: "omission", ...(role !== undefined ? { role } : {}), ...common, reason: text, ...fields } :
208
+ { kind: "excerpt", role, ...common, text, ...fields };
209
+ }
210
+ return record;
211
+ };
212
+ /** The structure of a record, whichever format it was read from: roles, source times, spans and, for the v1-only kinds, receipts. */
213
+ const validate = (r) => {
214
+ if (!r || typeof r !== "object" || Array.isArray(r))
215
+ throw new Error("Invalid pi-tuck transcript record");
216
+ const v = r;
217
+ const roles = ["user", "assistant", "custom", "bashExecution", "toolResult"];
218
+ const integer = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
219
+ if (!["excerpt", "tool_call", "tool_receipt", "tool_excerpt", "bash", "omission", "section"].includes(v.kind) ||
220
+ (v.role !== undefined && !roles.includes(v.role)) ||
221
+ (v.sourceTime !== undefined && (v.sourceEntryId === undefined || !validSourceTime(v.sourceTime))) ||
222
+ (v.sourceTimes !== undefined && (!Array.isArray(v.sourceTimes) || !v.sourceTimes.length || v.sourceTimes.some(t => !integer(t.sourceIndex) || !validSourceTime(Object.fromEntries(Object.entries(t).filter(([key]) => key !== "sourceIndex")))))) ||
223
+ (v.sourceIndex !== undefined && !integer(v.sourceIndex)) ||
224
+ (v.sourceEntryId !== undefined && (typeof v.sourceEntryId !== "string" || !v.sourceEntryId || v.sourceEntryId.length > 200 ||
225
+ v.role !== "custom" || !["excerpt", "omission"].includes(v.kind))) ||
226
+ (v.sourceIndices !== undefined && (!Array.isArray(v.sourceIndices) || !v.sourceIndices.every(integer))) ||
227
+ (v.kind !== "omission" && typeof v.text !== "string") ||
228
+ (v.kind === "omission" && typeof v.reason !== "string") ||
229
+ (v.kind === "excerpt" && !["user", "assistant", "custom"].includes(v.role)) ||
230
+ (v.kind === "tool_call" && v.role !== "assistant") ||
231
+ (v.kind === "bash" && v.role !== "bashExecution") ||
232
+ (v.kind === "section" && typeof v.title !== "string") ||
233
+ (v.count !== undefined && !integer(v.count)) ||
234
+ (v.span !== undefined && (v.span.coordinateSpace !== "sanitized-block" || !integer(v.span.start) || !integer(v.span.end) || !integer(v.span.totalCharacters) ||
235
+ v.span.start > v.span.end || v.span.end > v.span.totalCharacters ||
236
+ (v.kind === "excerpt" && v.text.length !== v.span.end - v.span.start))))
237
+ throw new Error("Invalid pi-tuck transcript record");
238
+ if (v.sourceTimes) {
239
+ const expected = v.sourceIndices ?? (v.sourceIndex === undefined ? [] : [v.sourceIndex]);
240
+ if (v.sourceTimes.length !== expected.length || v.sourceTimes.some((t, i) => t.sourceIndex !== expected[i]))
241
+ throw new Error("Mismatched source times");
242
+ }
243
+ if (["arguments", "argumentsJSONPrefix", "completeArgumentMembers"].some(key => Object.hasOwn(v, key))) {
244
+ if (v.kind !== "tool_call" ||
245
+ (Object.hasOwn(v, "arguments") && (!smallOriginalArguments(v.arguments, Number.MAX_SAFE_INTEGER) ||
246
+ Object.hasOwn(v, "argumentsJSONPrefix") || Object.hasOwn(v, "completeArgumentMembers"))) ||
247
+ (Object.hasOwn(v, "argumentsJSONPrefix") && (typeof v.argumentsJSONPrefix !== "string" || !v.argumentsJSONPrefix)) ||
248
+ (Object.hasOwn(v, "completeArgumentMembers") && (!Object.hasOwn(v, "argumentsJSONPrefix") ||
249
+ !smallOriginalArguments(v.completeArgumentMembers, Number.MAX_SAFE_INTEGER) || !Object.keys(v.completeArgumentMembers).length)))
250
+ throw new Error("Invalid historical argument fields");
251
+ }
252
+ if (v.kind === "tool_receipt" || v.kind === "tool_excerpt") {
253
+ const p = v.producer;
254
+ const keys = [...(v.sourceTimes ? ["sourceTimes"] : []), "kind", "role", "sourceIndex", "sourceIndices", "historical", "isError", "text", "producer", ...(v.kind === "tool_excerpt" ? ["complete", "notice", "range"] : [])];
255
+ if (Object.keys(v).length !== keys.length || !Object.keys(v).every(k => keys.includes(k)) ||
256
+ v.role !== "toolResult" || v.historical !== true || typeof v.isError !== "boolean" ||
257
+ !v.text || v.text.length > MAX_RECEIPT_CHARACTERS || !integer(v.sourceIndex) ||
258
+ !p || typeof p !== "object" || Array.isArray(p) || Object.keys(p).length !== 4 ||
259
+ !Object.keys(p).every(k => ["sourceIndex", "toolName", "toolCallId", "arguments", "argumentsOmitted"].includes(k)) ||
260
+ !integer(p.sourceIndex) || p.sourceIndex >= v.sourceIndex ||
261
+ typeof p.toolName !== "string" || !p.toolName.trim() || p.toolName.length > 160 ||
262
+ typeof p.toolCallId !== "string" || !p.toolCallId || p.toolCallId.length > 160 ||
263
+ !(Object.hasOwn(p, "arguments") && !Object.hasOwn(p, "argumentsOmitted") && smallOriginalArguments(p.arguments) ||
264
+ p.argumentsOmitted === true && !Object.hasOwn(p, "arguments")) ||
265
+ !Array.isArray(v.sourceIndices) || v.sourceIndices.length !== 2 ||
266
+ v.sourceIndices[0] !== p.sourceIndex || v.sourceIndices[1] !== v.sourceIndex)
267
+ throw new Error("Invalid historical receipt record");
268
+ if (v.kind === "tool_excerpt" && (v.complete !== false || ![RECEIPT_RANGE_NOTICE, LEGACY_RECEIPT_RANGE_NOTICE].includes(v.notice) || !v.range ||
269
+ Object.keys(v.range).length !== 4 || !Object.keys(v.range).every(k => ["coordinateSpace", "start", "end", "totalCharacters"].includes(k)) ||
270
+ v.range.coordinateSpace !== "readable-entry-utf16" || !integer(v.range.start) || !integer(v.range.end) || !integer(v.range.totalCharacters) ||
271
+ v.range.start >= v.range.end || v.range.end > v.range.totalCharacters || v.range.totalCharacters <= MAX_RECEIPT_CHARACTERS ||
272
+ v.text.length !== v.range.end - v.range.start))
273
+ throw new Error("Invalid historical receipt range");
274
+ }
275
+ return v;
276
+ };
277
+ /** Accept our explicit record protocol only. Never recover roles from payload text. `version` is the summary header's: v1 reads one JSON
278
+ * object per line, v2 the lines described above. Left out, it is taken from the first line, which a v1 record always begins with "{" and a v2 never. */
279
+ export const parseRecords = (text, version = text.startsWith("{") ? 1 : 2) => {
48
280
  if (!text)
49
281
  return [];
50
- return text.split("\n").map(line => {
51
- const r = JSON.parse(line);
52
- if (!r || typeof r !== "object" || Array.isArray(r))
53
- throw new Error("Invalid pi-tuck transcript record");
54
- const v = r;
55
- const roles = ["user", "assistant", "custom", "bashExecution", "toolResult"];
56
- const integer = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
57
- if (!["excerpt", "tool_call", "tool_receipt", "tool_excerpt", "bash", "omission", "section"].includes(v.kind) ||
58
- (v.role !== undefined && !roles.includes(v.role)) ||
59
- (v.sourceTime !== undefined && (v.sourceEntryId === undefined || !validSourceTime(v.sourceTime))) ||
60
- (v.sourceTimes !== undefined && (!Array.isArray(v.sourceTimes) || !v.sourceTimes.length || v.sourceTimes.some(t => !integer(t.sourceIndex) || !validSourceTime(Object.fromEntries(Object.entries(t).filter(([key]) => key !== "sourceIndex")))))) ||
61
- (v.sourceIndex !== undefined && !integer(v.sourceIndex)) ||
62
- (v.sourceEntryId !== undefined && (typeof v.sourceEntryId !== "string" || !v.sourceEntryId || v.sourceEntryId.length > 200 ||
63
- v.role !== "custom" || !["excerpt", "omission"].includes(v.kind))) ||
64
- (v.sourceIndices !== undefined && (!Array.isArray(v.sourceIndices) || !v.sourceIndices.every(integer))) ||
65
- (v.kind !== "omission" && typeof v.text !== "string") ||
66
- (v.kind === "omission" && typeof v.reason !== "string") ||
67
- (v.kind === "excerpt" && !["user", "assistant", "custom"].includes(v.role)) ||
68
- (v.kind === "tool_call" && v.role !== "assistant") ||
69
- (v.kind === "bash" && v.role !== "bashExecution") ||
70
- (v.kind === "section" && typeof v.title !== "string") ||
71
- (v.count !== undefined && !integer(v.count)) ||
72
- (v.span !== undefined && (v.span.coordinateSpace !== "sanitized-block" || !integer(v.span.start) || !integer(v.span.end) || !integer(v.span.totalCharacters) ||
73
- v.span.start > v.span.end || v.span.end > v.span.totalCharacters ||
74
- (v.kind === "excerpt" && v.text.length !== v.span.end - v.span.start))))
75
- throw new Error("Invalid pi-tuck transcript record");
76
- if (v.sourceTimes) {
77
- const expected = v.sourceIndices ?? (v.sourceIndex === undefined ? [] : [v.sourceIndex]);
78
- if (v.sourceTimes.length !== expected.length || v.sourceTimes.some((t, i) => t.sourceIndex !== expected[i]))
79
- throw new Error("Mismatched source times");
80
- }
81
- if (["arguments", "argumentsJSONPrefix", "completeArgumentMembers"].some(key => Object.hasOwn(v, key))) {
82
- if (v.kind !== "tool_call" ||
83
- (Object.hasOwn(v, "arguments") && (!smallOriginalArguments(v.arguments, Number.MAX_SAFE_INTEGER) ||
84
- Object.hasOwn(v, "argumentsJSONPrefix") || Object.hasOwn(v, "completeArgumentMembers"))) ||
85
- (Object.hasOwn(v, "argumentsJSONPrefix") && (typeof v.argumentsJSONPrefix !== "string" || !v.argumentsJSONPrefix)) ||
86
- (Object.hasOwn(v, "completeArgumentMembers") && (!Object.hasOwn(v, "argumentsJSONPrefix") ||
87
- !smallOriginalArguments(v.completeArgumentMembers, Number.MAX_SAFE_INTEGER) || !Object.keys(v.completeArgumentMembers).length)))
88
- throw new Error("Invalid historical argument fields");
89
- }
90
- if (v.kind === "tool_receipt" || v.kind === "tool_excerpt") {
91
- const p = v.producer;
92
- const keys = [...(v.sourceTimes ? ["sourceTimes"] : []), "kind", "role", "sourceIndex", "sourceIndices", "historical", "isError", "text", "producer", ...(v.kind === "tool_excerpt" ? ["complete", "notice", "range"] : [])];
93
- if (Object.keys(v).length !== keys.length || !Object.keys(v).every(k => keys.includes(k)) ||
94
- v.role !== "toolResult" || v.historical !== true || typeof v.isError !== "boolean" ||
95
- !v.text || v.text.length > MAX_RECEIPT_CHARACTERS || !integer(v.sourceIndex) ||
96
- !p || typeof p !== "object" || Array.isArray(p) || Object.keys(p).length !== 4 ||
97
- !Object.keys(p).every(k => ["sourceIndex", "toolName", "toolCallId", "arguments", "argumentsOmitted"].includes(k)) ||
98
- !integer(p.sourceIndex) || p.sourceIndex >= v.sourceIndex ||
99
- typeof p.toolName !== "string" || !p.toolName.trim() || p.toolName.length > 160 ||
100
- typeof p.toolCallId !== "string" || !p.toolCallId || p.toolCallId.length > 160 ||
101
- !(Object.hasOwn(p, "arguments") && !Object.hasOwn(p, "argumentsOmitted") && smallOriginalArguments(p.arguments) ||
102
- p.argumentsOmitted === true && !Object.hasOwn(p, "arguments")) ||
103
- !Array.isArray(v.sourceIndices) || v.sourceIndices.length !== 2 ||
104
- v.sourceIndices[0] !== p.sourceIndex || v.sourceIndices[1] !== v.sourceIndex)
105
- throw new Error("Invalid historical receipt record");
106
- if (v.kind === "tool_excerpt" && (v.complete !== false || ![RECEIPT_RANGE_NOTICE, LEGACY_RECEIPT_RANGE_NOTICE].includes(v.notice) || !v.range ||
107
- Object.keys(v.range).length !== 4 || !Object.keys(v.range).every(k => ["coordinateSpace", "start", "end", "totalCharacters"].includes(k)) ||
108
- v.range.coordinateSpace !== "readable-entry-utf16" || !integer(v.range.start) || !integer(v.range.end) || !integer(v.range.totalCharacters) ||
109
- v.range.start >= v.range.end || v.range.end > v.range.totalCharacters || v.range.totalCharacters <= MAX_RECEIPT_CHARACTERS ||
110
- v.text.length !== v.range.end - v.range.start))
111
- throw new Error("Invalid historical receipt range");
112
- }
113
- return v;
114
- });
282
+ return text.split("\n").map(line => validate(version === 1 ? JSON.parse(line) : parseLine(line)));
115
283
  };
@@ -1,17 +1,6 @@
1
1
  import type { convertToLlm } from "@earendil-works/pi-coding-agent";
2
2
  /** Includes Pi bash executions as well as ordinary LLM messages. */
3
3
  export type CompilerMessage = Parameters<typeof convertToLlm>[0][number];
4
- export interface FileOps {
5
- readFiles?: string[];
6
- modifiedFiles?: string[];
7
- createdFiles?: string[];
8
- }
9
- export interface ReceiptLink {
10
- status: "missing" | "ambiguous" | "error" | "non-error";
11
- sourceIndices: number[];
12
- /** Every original call/receipt contributing to the derived status. */
13
- dependencyIndices: number[];
14
- }
15
4
  export interface HistoricalReceipt {
16
5
  text: string;
17
6
  isError: boolean;
@@ -27,45 +16,39 @@ export interface HistoricalReceipt {
27
16
  argumentsOmitted: true;
28
17
  });
29
18
  }
19
+ /** `startsSource`: the block's original text begins the message's readable text, so an offset into it is a tuck_recall offset into the source. */
30
20
  export type NormalizedBlock = {
31
- sourceTime?: import("./core/source-time.js").SourceTime;
32
21
  sourceTimes?: Record<number, import("./core/source-time.js").SourceTime>;
33
22
  } & ({
34
23
  kind: "user";
35
24
  text: string;
36
25
  originalText?: string;
37
26
  sourceIndex?: number;
27
+ startsSource?: true;
38
28
  } | {
39
29
  kind: "assistant";
40
30
  text: string;
41
31
  originalText?: string;
42
32
  sourceIndex?: number;
33
+ startsSource?: true;
43
34
  } | {
44
35
  kind: "notice";
45
36
  text: string;
46
37
  originalText?: string;
47
38
  sourceIndex?: number;
48
39
  sourceEntryId?: string;
40
+ startsSource?: true;
49
41
  } | {
50
42
  kind: "tool_call";
51
43
  name: string;
52
44
  args: Record<string, unknown>;
53
- callId?: string;
54
- receipt?: ReceiptLink;
55
45
  sourceIndex?: number;
56
46
  } | {
57
47
  kind: "tool_result";
58
48
  name: string;
59
- text: string;
60
49
  sourceIndex?: number;
61
50
  recallExport?: true;
62
- originalReceipt?: HistoricalReceipt;
63
51
  } | {
64
52
  kind: "bash";
65
- command: string;
66
- output: string;
67
- exitCode: number | undefined;
68
- cancelled?: boolean;
69
- truncated?: boolean;
70
53
  sourceIndex?: number;
71
54
  });
@@ -6,8 +6,8 @@ type AgentMessage = Parameters<typeof convertToLlm>[0][number];
6
6
  type Cut = import("./tuck/cut.js").OwnCutResult;
7
7
  /** Counts a projection of the conversation exactly as the triggers, the post-cut checks and the guard do (budget.ts). */
8
8
  export type Measure = (messages: AgentMessage[]) => number;
9
- /** The summary view that produced a draft: the whole pre-cut dialogue, or the ranked fallback. Both carry the file ledger. */
10
- export type DraftMode = "dialogue" | "ranked";
9
+ /** The summary view that produced a draft: the whole pre-cut dialogue, or the same dialogue shrunk to the allowance. Both carry the file ledger. */
10
+ export type DraftMode = "dialogue" | "shrunk";
11
11
  type Draft = {
12
12
  compaction: Pick<Extract<SessionEntry, {
13
13
  type: "compaction";
@@ -48,7 +48,8 @@ export interface Tuck {
48
48
  charsPerToken: number;
49
49
  };
50
50
  buildGlobalIndexById(entries: readonly SessionEntry[]): Map<string, number>;
51
- compileRanked(input: {
51
+ /** The dialogue shrunk to `ranking.maxBriefChars`, oldest assistant text first; "" when not even the least of it fits. */
52
+ compileShrunkDialogue(input: {
52
53
  messages: AgentMessage[];
53
54
  sourceIndices: (number | undefined)[];
54
55
  sourceEntryIds?: readonly string[];
@@ -59,14 +60,11 @@ export interface Tuck {
59
60
  };
60
61
  ranking: {
61
62
  maxBriefChars: number;
62
- maxBriefCharsCeiling?: number;
63
- briefCharsPerBlock?: number;
64
- maxBriefUnits?: number;
65
63
  mandatorySourceIndices?: ReadonlySet<number>;
66
64
  };
67
65
  }): string;
68
- /** The whole pre-cut dialogue view; "" offers none, and the ranked view is used. */
69
- compileDialogue(input: Parameters<Tuck["compileRanked"]>[0]): string;
66
+ /** The whole pre-cut dialogue view; "" offers none, and the shrunk view is used. */
67
+ compileDialogue(input: Parameters<Tuck["compileShrunkDialogue"]>[0]): string;
70
68
  prepareKeywordQuery(query: string): PreparedKeywordQuery;
71
69
  searchEntriesDetailed(entries: SearchHit[], messages: Message[], query: string, tuning: {
72
70
  relativeFloor: number;
@@ -78,11 +76,11 @@ export interface Tuck {
78
76
  export declare const tuck: Tuck;
79
77
  /** The one historical allowance: at most 25% of remaining estimated room, for both views.
80
78
  * The whole dialogue is offered only within it (plus actual preview/headroom guards);
81
- * otherwise the ranked view fills it. The 4-character conversion matches pinned Pi's
79
+ * otherwise the same dialogue is shrunk to fit it. The 4-character conversion matches pinned Pi's
82
80
  * summary estimator, not a provider-token guarantee. Mandatory sources (current input,
83
81
  * initiating prompt, file ledger) are already charged in base. */
84
82
  export declare function historicalAllowance(window: number, reserve: number, base: number, before: number): number;
85
- /** Uses pi-tuck's own cut, calibrated maximum tail and ranked compiler; Pi owns persistence and continuation. */
83
+ /** Uses pi-tuck's own cut, calibrated maximum tail and dialogue compiler; Pi owns persistence and continuation. */
86
84
  export declare function tuckDraft(tuck: Tuck, event: SessionBeforeCompactEvent, ctx: ExtensionContext, reserveTokens: number, measure: Measure, policy?: RecallPolicy, retainCurrentInput?: boolean, operatorManual?: boolean): Draft;
87
85
  /** Shared compiler observes canonical branch and current token estimate; no native preparation utility. */
88
86
  export declare function tuckObservedDraft(tuck: Tuck, input: {
package/dist/src/tuck.js CHANGED
@@ -2,7 +2,7 @@ import { resolveSourceTime } from "./tuck/core/source-time.js";
2
2
  import { orderByOriginalSource } from "./tuck/core/source-order.js";
3
3
  import { assertNativeToolRounds, isDiscardedResponse } from "./protocol.js";
4
4
  import { parseRecords, serializeTranscript, splitSummary } from "./tuck/core/transcript.js";
5
- import { DIALOGUE_SUFFIX, RANKED_SUFFIX } from "./tuck/core/format.js";
5
+ import { DIALOGUE_SUFFIX } from "./tuck/core/format.js";
6
6
  import { isTextOnly, textOf } from "./tuck/core/content.js";
7
7
  import { MAX_TASK_CONTRACT_CHARACTERS, actionItems, actionRecords, head, ledgerItems, ledgerRecords } from "./ledger.js";
8
8
  import { SessionManager } from "@earendil-works/pi-coding-agent";
@@ -12,20 +12,20 @@ export const MAX_RANKED_RESPONSE_CHARACTERS = 16000;
12
12
  import { buildOwnCut, applyTailBudget, MAX_TAIL_TOKENS, OVERSIZED_TAIL_FACTOR } from "./tuck/cut.js";
13
13
  import { calibrateCharsPerToken, estimateSourceMessageChars } from "./tuck/core/token-estimate.js";
14
14
  import { buildGlobalIndexById } from "./tuck/core/global-indices.js";
15
- import { compileRanked, compileDialogue } from "./tuck/core/summarize.js";
15
+ import { compileShrunkDialogue, compileDialogue } from "./tuck/core/summarize.js";
16
16
  import { prepareKeywordQuery } from "./tuck/core/keyword-query.js";
17
17
  import { searchEntriesDetailed } from "./tuck/core/search-entries.js";
18
- export const tuck = { buildOwnCut, applyTailBudget, calibrateCharsPerToken, buildGlobalIndexById, compileRanked, compileDialogue, prepareKeywordQuery, searchEntriesDetailed };
18
+ export const tuck = { buildOwnCut, applyTailBudget, calibrateCharsPerToken, buildGlobalIndexById, compileShrunkDialogue, compileDialogue, prepareKeywordQuery, searchEntriesDetailed };
19
19
  /** The one historical allowance: at most 25% of remaining estimated room, for both views.
20
20
  * The whole dialogue is offered only within it (plus actual preview/headroom guards);
21
- * otherwise the ranked view fills it. The 4-character conversion matches pinned Pi's
21
+ * otherwise the same dialogue is shrunk to fit it. The 4-character conversion matches pinned Pi's
22
22
  * summary estimator, not a provider-token guarantee. Mandatory sources (current input,
23
23
  * initiating prompt, file ledger) are already charged in base. */
24
24
  export function historicalAllowance(window, reserve, base, before) {
25
25
  const free = Math.max(0, window - reserve - base - 1);
26
26
  return Math.max(0, Math.min(Math.floor(free / 4), before - base - 1)) * 4;
27
27
  }
28
- /** Uses pi-tuck's own cut, calibrated maximum tail and ranked compiler; Pi owns persistence and continuation. */
28
+ /** Uses pi-tuck's own cut, calibrated maximum tail and dialogue compiler; Pi owns persistence and continuation. */
29
29
  export function tuckDraft(tuck, event, ctx, reserveTokens, measure, policy = {}, retainCurrentInput = true, operatorManual = false) {
30
30
  return tuckObservedDraft(tuck, { branchEntries: event.branchEntries, tokensBefore: event.preparation.tokensBefore, previousSummary: event.preparation.previousSummary, operatorManual }, ctx, reserveTokens, measure, policy, retainCurrentInput);
31
31
  }
@@ -50,9 +50,9 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
50
50
  const chars = (calibration.ok ? calibration.messages.reduce((n, m) => n + estimateSourceMessageChars(m), 0) : 0)
51
51
  + (input.previousSummary?.length ?? 0);
52
52
  const { charsPerToken } = tuck.calibrateCharsPerToken(chars, input.tokensBefore);
53
- // A preferred tail cannot consume more than the current input allowance.
54
- // Current/fresh-source floors below may still require an explicit rejection.
55
- const inputRoom = Math.max(0, (ctx.model?.contextWindow ?? 0) - reserveTokens - 1);
53
+ // A preferred tail takes at most half the current input allowance, so a cut leaves room to grow: a tail that fills the room lands the
54
+ // request just under the band and the next round cuts again. Current/fresh-source floors below may still require an explicit rejection.
55
+ const inputRoom = Math.floor(Math.max(0, (ctx.model?.contextWindow ?? 0) - reserveTokens - 1) / 2);
56
56
  const maxTokens = Math.min(MAX_TAIL_TOKENS, inputRoom);
57
57
  // Keep the current user turn native; older conversation competes in the
58
58
  // existing exact historical view instead of boosting a minimum native tail.
@@ -164,14 +164,14 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
164
164
  throw new Error("Missing initiating user source index");
165
165
  const quoted = head(text, MAX_TASK_CONTRACT_CHARACTERS), sourceTimes = [{ sourceIndex: index, ...resolveSourceTime(initiating) }];
166
166
  if (text.trim()) {
167
- mandatory.push({ kind: "excerpt", role: "user", sourceIndex: index, sourceTimes, text: quoted });
167
+ mandatory.push({ kind: "excerpt", role: "user", sourceIndex: index, text: quoted, sourceTimes });
168
168
  if (quoted.length < text.length)
169
- mandatory.push({ kind: "omission", role: "user", sourceIndex: index, sourceTimes,
170
- reason: `Initiating prompt continues: characters ${quoted.length}-${text.length} omitted; read them with tuck_recall {"index":${index},"offset":${quoted.length}}` });
169
+ mandatory.push({ kind: "omission", role: "user", sourceIndex: index,
170
+ reason: `Initiating prompt continues: characters ${quoted.length}-${text.length} omitted; read them with tuck_recall {"index":${index},"offset":${quoted.length}}`, sourceTimes });
171
171
  }
172
172
  }
173
173
  if (protectedUser?.type === "message" && protectedUser.message.role === "user")
174
- mandatory.push({ kind: "excerpt", role: "user", sourceIndex: currentIndex, sourceTimes: [{ sourceIndex: currentIndex, ...resolveSourceTime(protectedUser) }], text: textOf(protectedUser.message.content) });
174
+ mandatory.push({ kind: "excerpt", role: "user", sourceIndex: currentIndex, text: textOf(protectedUser.message.content), sourceTimes: [{ sourceIndex: currentIndex, ...resolveSourceTime(protectedUser) }] });
175
175
  // The file ledger and the action trail of the compacted history are mandatory, so they are charged in base like the quotes above.
176
176
  // A locator is given only where tuck_recall {index} resolves it: a unique global index of a readable entry.
177
177
  const compacted = entries.slice(0, boundary), resolves = (entry) => readable(entry) !== undefined;
@@ -190,8 +190,7 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
190
190
  const model = ctx.model;
191
191
  const floorAt = (keep) => {
192
192
  const ledger = [...actionRecords(actions, keep), ...ledgerRecords(items, keep)];
193
- // Charged with the longer (ranked) closing, since the view is chosen later against this base.
194
- return { ledger, base: measure(preview(serializeTranscript([...mandatory, ...ledger]) + RANKED_SUFFIX)) };
193
+ return { ledger, base: measure(preview(serializeTranscript([...mandatory, ...ledger]) + DIALOGUE_SUFFIX)) };
195
194
  };
196
195
  const fits = (base) => Boolean(model) && base + reserveTokens < model.contextWindow && base < before;
197
196
  // When the whole floor cannot fit, the trail and the ledger keep only their headings and counts; only that can refuse the cut.
@@ -204,15 +203,14 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
204
203
  const maxBriefChars = historicalAllowance(model.contextWindow, reserveTokens, base, before);
205
204
  const compileInput = { messages, sourceIndices, sourceEntryIds: originalCut.selectedIds, sourceTimes: originalCut.selectedIds.map(id => resolveSourceTime(entryById.get(id))), receiptContext,
206
205
  ranking: { maxBriefChars, mandatorySourceIndices: new Set(mandatory.flatMap(r => r.sourceIndex === undefined ? [] : [r.sourceIndex])) }, };
207
- // All offers use the same parser, mandatory insertion, native preview and guard.
208
- // Each view ends with its own closing section, true for what that view holds.
209
- const assemble = (compiled, closing) => {
206
+ // Both views use the same parser, mandatory insertion, native preview and guard, and end with the same closing section.
207
+ const assemble = (compiled) => {
210
208
  if (typeof compiled !== "string")
211
209
  throw new Error("pi-tuck returned an invalid summary");
212
- const split = compiled ? splitSummary(compiled) : { body: "" };
210
+ const split = compiled ? splitSummary(compiled) : { body: "", version: 2 };
213
211
  if (!split)
214
212
  throw new Error("pi-tuck returned an invalid summary");
215
- const records = parseRecords(split.body);
213
+ const records = parseRecords(split.body, split.version);
216
214
  for (const record of mandatory) {
217
215
  const insertion = records.findIndex(row => row.sourceIndex !== undefined && row.sourceIndex > record.sourceIndex);
218
216
  records.splice(insertion < 0 ? records.length : insertion, 0, record);
@@ -220,29 +218,29 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
220
218
  orderByOriginalSource(records, sourceIndices, originalCut.selectedIds);
221
219
  // The ledger always has its heading, so no summary is empty.
222
220
  records.push(...ledger);
223
- const summary = serializeTranscript(records) + closing;
221
+ const summary = serializeTranscript(records) + DIALOGUE_SUFFIX;
224
222
  const projection = preview(summary);
225
223
  assertNativeToolRounds(projection);
226
224
  return { summary, after: measure(projection) };
227
225
  };
228
226
  const free = Math.max(0, model.contextWindow - reserveTokens - base - 1);
229
227
  const headroom = Math.ceil(free / 4);
230
- // The whole pre-cut dialogue, when it fits the allowance; otherwise the ranked view fills the same allowance.
228
+ // The whole pre-cut dialogue, when it fits the allowance; otherwise the same dialogue shrunk to the same allowance.
231
229
  // With no allowance only an operator's manual cut can use it.
232
230
  const dialogue = maxBriefChars > 0 || input.operatorManual ? tuck.compileDialogue(compileInput) : "";
233
231
  const dialogueFits = dialogue.length <= maxBriefChars;
234
- const candidate = dialogue && (dialogueFits || input.operatorManual) ? assemble(dialogue, DIALOGUE_SUFFIX) : undefined;
232
+ const candidate = dialogue && (dialogueFits || input.operatorManual) ? assemble(dialogue) : undefined;
235
233
  const offer = candidate && dialogueFits && candidate.after < before && candidate.after + reserveTokens + headroom < model.contextWindow ? candidate : undefined;
236
234
  // Manual no-work preserves native originals; final request admission still owns full overhead.
237
235
  if (!offer && input.operatorManual && candidate && candidate.after >= before &&
238
236
  before + reserveTokens < model.contextWindow)
239
237
  return { unavailable: "Manual compaction preserved original dialogue: the complete-dialogue view does not reduce context" };
240
- const { summary, after } = offer ?? assemble(tuck.compileRanked(compileInput), RANKED_SUFFIX);
238
+ const { summary, after } = offer ?? assemble(tuck.compileShrunkDialogue(compileInput));
241
239
  // Draft reduction concerns conversation. Final request admission owns the effective system/tools.
242
240
  if (after + reserveTokens >= model.contextWindow || after >= before)
243
241
  return { unavailable: "pi-tuck's draft does not reduce context within the request budget", capacityRejected: after + reserveTokens >= model.contextWindow };
244
- const mode = offer ? "dialogue" : "ranked";
245
- return { compaction: { summary, firstKeptEntryId: cut.firstKeptEntryId, tokensBefore: input.tokensBefore, details: { ...details, tokensAfter: after, view: offer ? "complete-dialogue" : "ranked", mode } }, after, mode };
242
+ const mode = offer ? "dialogue" : "shrunk";
243
+ return { compaction: { summary, firstKeptEntryId: cut.firstKeptEntryId, tokensBefore: input.tokensBefore, details: { ...details, tokensAfter: after, view: offer ? "complete-dialogue" : "shrunk-dialogue", mode } }, after, mode };
246
244
  };
247
245
  const preferred = compileAtCut(cut);
248
246
  if (preferred.mode === "dialogue")