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
@@ -1,19 +1,5 @@
1
1
  import type { NormalizedBlock } from "../types";
2
- import { clip } from "./content";
3
- import type { SourceTime } from "./source-time";
4
- import { serializeRecords, transcriptUnits, TRANSCRIPT_UNIT_CHARACTERS, smallOriginalArguments, MAX_PRODUCER_ARGUMENT_CHARACTERS, type TranscriptRecord } from "./transcript";
5
-
6
- const TRUNCATE_USER = 256;
7
- const SEGMENT_CLOSING_ASSISTANT_HEAD_WORDS = 120;
8
- const SEGMENT_CLOSING_ASSISTANT_TAIL_WORDS = 120;
9
- const ASSISTANT_HEAD_WORDS = 80;
10
- const ASSISTANT_TAIL_WORDS = 120;
11
-
12
- // ── noise filtering ──
13
-
14
- const isNoiseUser = (text: string): boolean => {
15
- return !text.trim();
16
- };
2
+ import { escapedLength, type TranscriptRecord } from "./transcript";
17
3
 
18
4
  // ── truncation ──
19
5
 
@@ -98,288 +84,85 @@ const passageRanges = (text: string): { start: number; end: number }[] => {
98
84
  return ranges;
99
85
  };
100
86
 
101
- /** Existing word/serialized budgets choose complete source passages, never
102
- * rewritten prose. Physical line wrapping does not define a prose candidate. */
103
- const excerptRecords = (text: string, role: "user" | "assistant" | "custom", sourceIndex: number | undefined,
104
- headLimit: number, tailLimit = 0, boundedChars = false): TranscriptRecord[] => {
87
+ /** The passages of a text and the words each costs. Segmenting is most of the work of a clip, so a block is analyzed once for every clip. */
88
+ interface Analysis { passages: { start: number; end: number }[]; costs: number[] }
89
+ const analyze = (text: string): Analysis => {
105
90
  const passages = passageRanges(text);
106
- const costs = passages.map(({ start, end }) => [...segmenter.segment(text.slice(start, end))]
107
- .filter((seg) => isWord(seg) && !STOP_WORDS.has(seg.segment.toLowerCase())).length);
91
+ return { passages, costs: passages.map(({ start, end }) => [...segmenter.segment(text.slice(start, end))]
92
+ .filter((seg) => isWord(seg) && !STOP_WORDS.has(seg.segment.toLowerCase())).length) };
93
+ };
94
+
95
+ /** Existing word/serialized budgets choose complete source passages, never
96
+ * rewritten prose. Physical line wrapping does not define a prose candidate. A passage's serialized length is its length in a record line. */
97
+ const excerptRecords = (text: string, { passages, costs }: Analysis, role: "user" | "assistant" | "custom", sourceIndex: number | undefined,
98
+ headLimit: number, tailLimit: number, readFrom: (offset: number) => string): TranscriptRecord[] => {
108
99
  const charLimit = 1024;
109
100
  const identity = { role, ...(sourceIndex !== undefined ? { sourceIndex } : {}) };
110
- if ((!boundedChars || text.length <= charLimit && JSON.stringify(text).length <= charLimit) && costs.reduce((sum, cost) => sum + cost, 0) <= headLimit + tailLimit)
101
+ // A line carries at least as many characters as the text, so a long text is over the limit without counting its escapes.
102
+ const fits = (from: number, to: number) => to - from <= charLimit && escapedLength(text.slice(from, to)) <= charLimit;
103
+ if (fits(0, text.length) && costs.reduce((sum, cost) => sum + cost, 0) <= headLimit + tailLimit)
111
104
  return [{ kind: "excerpt", ...identity, text }];
112
105
  let head = 0, headCost = 0, tail = passages.length, tailCost = 0;
113
- while (head < tail && headCost + costs[head]! <= headLimit && (!boundedChars || JSON.stringify(text.slice(0, passages[head]!.end)).length <= charLimit)) { headCost += costs[head++]!; }
106
+ while (head < tail && headCost + costs[head]! <= headLimit && fits(0, passages[head]!.end)) { headCost += costs[head++]!; }
114
107
  if (tailLimit > 0) {
115
- while (tail > head && tailCost + costs[tail - 1]! <= tailLimit && (!boundedChars || JSON.stringify(text.slice(passages[tail - 1]!.start)).length <= charLimit)) { tailCost += costs[--tail]!; }
108
+ while (tail > head && tailCost + costs[tail - 1]! <= tailLimit && fits(passages[tail - 1]!.start, text.length)) { tailCost += costs[--tail]!; }
116
109
  }
117
110
  const headEnd = head ? passages[head - 1]!.end : 0;
118
111
  const tailStart = tail < passages.length ? passages[tail]!.start : text.length;
119
112
  const span = (start: number, end: number) => ({ coordinateSpace: "sanitized-block" as const, start, end, totalCharacters: text.length });
120
113
  const records: TranscriptRecord[] = [];
121
114
  if (headEnd) records.push({ kind: "excerpt", ...identity, text: text.slice(0, headEnd), span: span(0, headEnd) });
122
- records.push({ kind: "omission", ...identity, reason: "source truncated; read source; omitted text may qualify retained quotations", span: span(headEnd, tailStart) });
115
+ records.push({ kind: "omission", ...identity, reason: readFrom(headEnd), span: span(headEnd, tailStart) });
123
116
  if (tailStart < text.length) records.push({ kind: "excerpt", ...identity, text: text.slice(tailStart), span: span(tailStart, text.length) });
124
117
  return records;
125
118
  };
126
119
 
127
- const nextRenderableBlock = (blocks: NormalizedBlock[], index: number): NormalizedBlock | undefined => {
128
- for (let i = index + 1; i < blocks.length; i++) {
129
- if (blocks[i].kind !== "tool_result") return blocks[i];
130
- }
131
- return undefined;
132
- };
133
-
134
- export const isSegmentClosingAssistant = (blocks: NormalizedBlock[], index: number): boolean => {
135
- if (blocks[index]?.kind !== "assistant") return false;
136
- const next = nextRenderableBlock(blocks, index);
137
- return !next || next.kind === "user";
138
- };
139
-
140
- // ── bash command compression ──
141
-
142
- export const HEREDOC_OPEN_RE = /(?<![\w)])<<-?\s*["']?([A-Za-z_]\w*)["']?/;
143
- /**
144
- * If `lines[i]` opens a heredoc whose terminator actually appears on a later
145
- * line, return that terminator's line index; otherwise -1. Callers use -1 to
146
- * leave following lines intact instead of treating a stray `<<` (a shift op, a
147
- * quoted string, or a truncated body) as a heredoc and skipping real commands.
148
- */
149
- export const heredocCloseIndex = (lines: string[], i: number): number => {
150
- const hd = lines[i].match(HEREDOC_OPEN_RE);
151
- if (!hd) return -1;
152
- const term = hd[1];
153
- for (let j = i + 1; j < lines.length; j++) {
154
- if (lines[j].trim() === term) return j;
155
- }
156
- return -1;
157
- };
158
-
159
- /** Source-relative call identities remain explicitly bounded. */
160
- const excerpt = (text: string, limit: number): string =>
161
- JSON.stringify(clip(text, limit)) + (text.length > limit ? " [truncated; read source]" : "");
162
-
163
- /** Exact serialized prefix only after a lossless JSON roundtrip. No argument interpretation. */
164
- const incompletePrefix = (text: string): string => {
165
- let end = Math.min(text.length, MAX_PRODUCER_ARGUMENT_CHARACTERS);
166
- if (end < text.length && end > 0 && /[\uD800-\uDBFF]/u.test(text[end - 1]!) && /[\uDC00-\uDFFF]/u.test(text[end]!)) end--;
167
- return JSON.stringify(text.slice(0, end)) + " [incomplete prefix; omitted text may qualify it; read original source]";
168
- };
169
-
170
- /** Small complete arguments remain unchanged; oversized valid originals expose an exact prefix. */
171
- const toolOneLiner = (name: string, args: Record<string, unknown>): string =>
172
- `* ${name} ` + (smallOriginalArguments(args)
173
- ? `arguments=${JSON.stringify(args)}`
174
- : smallOriginalArguments(args, Number.MAX_SAFE_INTEGER)
175
- ? `serializedArgumentsPrefix=${incompletePrefix(JSON.stringify(args))}`
176
- : "arguments omitted; read original source");
177
-
178
- export type CallRecord = Extract<TranscriptRecord, { kind: "tool_call" }>;
179
- /** Literal JSON fields avoid quoting JSON inside quoted transcript text.
180
- * Keep the old leading excerpt before adding whole members. Its existing row
181
- * unit allowance bounds the offer; full character/admission owners still apply. */
182
- const literalArguments = (baseline: CallRecord, args: Record<string, unknown>, facts: string): CallRecord => {
183
- const budget = transcriptUnits(serializeRecords([baseline])) * TRANSCRIPT_UNIT_CHARACTERS;
184
- const charge = (record: CallRecord) => serializeRecords([record]).length;
185
- const complete: CallRecord = { ...baseline, text: facts, arguments: args };
186
- if (charge(complete) <= budget) return complete;
187
- const original = JSON.stringify(args);
188
- let end = Math.min(original.length, MAX_PRODUCER_ARGUMENT_CHARACTERS);
189
- if (end < original.length && /[\uD800-\uDBFF]/u.test(original[end - 1]!) && /[\uDC00-\uDFFF]/u.test(original[end]!)) end--;
190
- const record: CallRecord = { ...baseline,
191
- text: facts + " [incomplete arguments; omitted text may qualify them; read original source]",
192
- argumentsJSONPrefix: original.slice(0, end) };
193
- if (charge(record) > budget) return baseline;
194
- const members = Object.entries(args).map(([key, value], order) => ({ key, value, order,
195
- cost: JSON.stringify({ [key]: value }).length })).sort((a, b) => a.cost - b.cost || a.order - b.order);
196
- const selected: typeof members = [];
197
- const projected = () => Object.fromEntries([...selected].sort((a, b) => a.order - b.order).map(({ key, value }) => [key, value]));
198
- for (const member of members) {
199
- selected.push(member);
200
- if (charge({ ...record, completeArgumentMembers: projected() }) > budget) selected.pop();
201
- }
202
- if (selected.length) record.completeArgumentMembers = projected();
203
- // Search only complete code-point boundaries: escaped lone surrogates would
204
- // otherwise make encoded prefix prices non-monotonic.
205
- const ends = [end], ceiling = Math.min(original.length - 1, budget);
206
- while (end < ceiling) {
207
- const next = end + (/[\uD800-\uDBFF]/u.test(original[end]!) && /[\uDC00-\uDFFF]/u.test(original[end + 1]!) ? 2 : 1);
208
- if (next > ceiling) break;
209
- ends.push(end = next);
210
- }
211
- let low = 0, high = ends.length - 1;
212
- while (low < high) {
213
- const mid = Math.ceil((low + high) / 2);
214
- if (charge({ ...record, argumentsJSONPrefix: original.slice(0, ends[mid]) }) <= budget) low = mid;
215
- else high = mid - 1;
216
- }
217
- record.argumentsJSONPrefix = original.slice(0, ends[low]);
218
- return record;
219
- };
220
-
221
- /** Direct shell history quotes an exact command prefix, never an interpreted command. */
222
- const bashRecord = (command: string, exitCode: number | undefined): string =>
223
- (smallOriginalArguments({command}) ? `command=${JSON.stringify(command)}`
224
- : smallOriginalArguments({command}, Number.MAX_SAFE_INTEGER)
225
- ? command.length <= MAX_PRODUCER_ARGUMENT_CHARACTERS ? `command=${JSON.stringify(command)}` : `commandPrefix=${incompletePrefix(command)}`
226
- : "command omitted; read original source") +
227
- ` recordedExitCode=${exitCode === undefined ? "unknown" : String(exitCode)}`;
228
-
229
- /** One record is independently decodable; source text cannot create structure. */
230
- export type ExcerptCache = WeakMap<NormalizedBlock, Map<string, TranscriptRecord[]>>;
231
- // Ephemeral rendering decisions belong to the selected array, not source blocks.
232
- // Measurement and final rendering therefore use the same representation.
233
- interface BriefPlan { full: Set<NormalizedBlock>; closing: Set<NormalizedBlock>; mandatorySources?: ReadonlySet<number>; omissions: TranscriptRecord[] }
234
- const plans = new WeakMap<NormalizedBlock[], BriefPlan>();
235
-
236
- /** The assistant texts that close a segment: no further text before the next user turn or the end of the chronology. */
237
- export const segmentClosing = (chronology: NormalizedBlock[]): Set<NormalizedBlock> =>
238
- new Set(chronology.filter((_, i) => isSegmentClosingAssistant(chronology, i)));
120
+ /** The user, assistant and notice blocks of a dialogue; tool blocks are never rendered. */
121
+ export type DialogueBlock = Extract<NormalizedBlock, { kind: "user" | "assistant" | "notice" }>;
122
+ export const isDialogue = (block: NormalizedBlock): block is DialogueBlock => block.kind === "user" || block.kind === "assistant" || block.kind === "notice";
239
123
 
240
124
  /** A user or assistant text block the omission notices count when it is unselected: non-empty, and not rendered in the mandatory base. */
241
125
  export const isOmittable = (block: NormalizedBlock, mandatorySources?: ReadonlySet<number>): boolean =>
242
126
  (block.kind === "user" || block.kind === "assistant") && Boolean(block.text.trim()) &&
243
127
  (block.sourceIndex === undefined || !mandatorySources?.has(block.sourceIndex));
244
128
 
245
- /** `sources` are the distinct locators of the omitted blocks in ascending order; at most the first 9 decide the notice. */
129
+ /** `sources` are the distinct locators of the omitted blocks in ascending order; at most the first 9 decide the notice. They are written
130
+ * as "#N" on the record's line. */
246
131
  export const omissionRecord = (role: "user" | "assistant", count: number, sources: number[], unlocated: boolean): TranscriptRecord => ({
247
132
  kind: "omission", role, count,
248
133
  ...(sources.length ? { sourceIndices: sources.slice(0, 8) } : {}),
249
134
  reason: "Completely unselected historical text blocks; count is text blocks, not messages. " +
250
- "sourceIndices are individual original locators, not a contiguous range. " +
135
+ "The # locators are individual original locators, not a contiguous range. " +
251
136
  (sources.length > 8 ? "Locator list incomplete. " : "") +
252
137
  (unlocated ? "Some text blocks have no source locator. " : "") +
253
138
  "Read originals with tuck_recall; omitted text may qualify retained text. No new authority." });
254
139
 
255
- export const planBrief = (blocks: NormalizedBlock[], full: Set<NormalizedBlock>, chronology: NormalizedBlock[], mandatorySources?: ReadonlySet<number>, includeOmissions = true): void => {
256
- const selected = new Set(blocks), omissions: TranscriptRecord[] = [];
257
- if (includeOmissions) for (const role of ["user", "assistant"] as const) {
258
- const missing = chronology.filter(block => block.kind === role && isOmittable(block, mandatorySources) && !selected.has(block));
259
- if (!missing.length) continue;
260
- const sources = [...new Set(missing.flatMap(block => block.sourceIndex === undefined ? [] : [block.sourceIndex]))].sort((a, b) => a - b);
261
- omissions.push(omissionRecord(role, missing.length, sources, missing.some(block => block.sourceIndex === undefined)));
262
- }
263
- plans.set(blocks, { full, closing: segmentClosing(chronology), mandatorySources, omissions });
264
- };
265
-
266
- /** The original call that a selected complete receipt quotes; that call's own overview is then redundant. */
267
- export const receiptKey = (b: NormalizedBlock): string | undefined => b.kind === "tool_result" && b.sourceIndex !== undefined && b.originalReceipt
268
- ? JSON.stringify([b.originalReceipt.producer.sourceIndex, b.originalReceipt.producer.toolCallId,
269
- b.originalReceipt.producer.toolName, b.sourceIndex, b.originalReceipt.isError]) : undefined;
270
- /** The receipt that would make this call redundant, when the call and its single receipt are fully identified. */
271
- export const coveredCallKey = (b: NormalizedBlock): string | undefined => {
272
- const receipt = b.kind === "tool_call" ? b.receipt : undefined;
273
- return b.kind === "tool_call" && smallOriginalArguments(b.args) && b.sourceIndex !== undefined && receipt &&
274
- (receipt.status === "error" || receipt.status === "non-error") && receipt.sourceIndices.length === 1
275
- ? JSON.stringify([b.sourceIndex, b.callId, b.name, receipt.sourceIndices[0], receipt.status === "error"]) : undefined;
276
- };
277
-
278
- /** What one block renders in this brief, set by the plan and by the selected array around it. */
279
- export interface BlockView { full: boolean; closing: boolean; mandatory?: ReadonlySet<number>; bounded: boolean; covered: boolean }
280
-
281
- /** The records of one block alone: before identical calls merge and before source times are attached. */
282
- export const blockRecords = (b: NormalizedBlock, view: BlockView, cache?: ExcerptCache): TranscriptRecord[] => {
283
- const records: TranscriptRecord[] = [];
284
- const identity = b.sourceIndex !== undefined ? { sourceIndex: b.sourceIndex } : {};
285
- const excerpts = (block: NormalizedBlock & { text: string; originalText?: string }, role: "user" | "assistant" | "custom", head: number, tail = 0) => {
286
- // The adapter already renders these complete sources in its mandatory base.
287
- // Keep them visible to structural grouping without charging them twice.
288
- if (block.sourceIndex !== undefined && view.mandatory?.has(block.sourceIndex)) return [];
289
- if (view.full) return [{ kind: "excerpt" as const, role, ...identity, text: block.originalText ?? block.text, ...(block.kind === "notice" && block.sourceEntryId !== undefined ? { sourceEntryId: block.sourceEntryId } : {}) }];
290
- const key = `${head}:${tail}:${view.bounded}`;
291
- const saved = cache?.get(block)?.get(key);
292
- if (saved) return saved;
293
- const result = excerptRecords(block.text, role, block.sourceIndex, head, tail, view.bounded).map(record =>
294
- block.kind === "notice" && block.sourceEntryId !== undefined ? { ...record, sourceEntryId: block.sourceEntryId } : record);
295
- if (cache) {
296
- let variants = cache.get(block);
297
- if (!variants) { variants = new Map(); cache.set(block, variants); }
298
- variants.set(key, result);
299
- }
300
- return result;
301
- };
302
- if (b.kind === "user" || b.kind === "notice" || b.kind === "bash") {
303
- if (b.kind === "bash") {
304
- const stored = view.full ? "\nstoredShell=" + JSON.stringify({ historical: true,
305
- output: b.output, cancelled: b.cancelled ?? "unknown", truncated: b.truncated ?? "unknown" }) : "";
306
- records.push({ kind: "bash", role: "bashExecution", ...identity, text: bashRecord(b.command, b.exitCode) + stored });
307
- } else if (!isNoiseUser(b.text)) {
308
- records.push(...excerpts(b, b.kind === "user" ? "user" : "custom", TRUNCATE_USER));
309
- }
310
- } else if (b.kind === "assistant") {
311
- if (b.text) records.push(...excerpts(b, "assistant",
312
- view.closing ? SEGMENT_CLOSING_ASSISTANT_HEAD_WORDS : ASSISTANT_HEAD_WORDS,
313
- view.closing ? SEGMENT_CLOSING_ASSISTANT_TAIL_WORDS : ASSISTANT_TAIL_WORDS));
314
- } else if (b.kind === "tool_call") {
315
- if (!b.name.trim() || view.covered) return records;
316
- const receipt = b.receipt;
317
- const relation = receipt ? `; receipt=${receipt.status}` +
318
- (receipt.sourceIndices.length ? ` source=${receipt.sourceIndices.map(n => `#${n}`).join(",")}` : "") : "";
319
- const suffix = (b.callId ? ` call=${excerpt(b.callId, 160)}` : "") + relation;
320
- const text = toolOneLiner(b.name, b.args) + suffix;
321
- const sourceIndices = b.sourceIndex === undefined ? undefined :
322
- [...new Set([b.sourceIndex, ...(receipt?.dependencyIndices ?? [])])].sort((a, b) => a - b);
323
- const baseline: CallRecord = { kind: "tool_call", role: "assistant", ...identity,
324
- ...(sourceIndices && sourceIndices.length > 1 ? { sourceIndices } : {}), text };
325
- records.push(!smallOriginalArguments(b.args) && smallOriginalArguments(b.args, Number.MAX_SAFE_INTEGER)
326
- ? literalArguments(baseline, b.args, `* ${b.name}${suffix}`) : baseline);
327
- }
328
- if (b.kind === "tool_result" && b.originalReceipt && b.sourceIndex !== undefined) {
329
- const r = b.originalReceipt;
330
- // One historical quote carries its own addressed producer, without a
331
- // redundant overview for the same original event. Never create native tool messages.
332
- records.push({ kind: "tool_receipt", role: "toolResult", sourceIndex: b.sourceIndex,
333
- sourceIndices: [r.producer.sourceIndex, b.sourceIndex], historical: true,
334
- isError: r.isError, text: r.text, producer: r.producer });
335
- }
336
- return records;
337
- };
338
-
339
- /** Derived tool summaries can share wording; provenance remains explicit. */
340
- const payloads = new WeakMap<CallRecord, string>();
341
- const callPayload = (r: CallRecord): string => {
342
- let payload = payloads.get(r);
343
- if (payload === undefined) payloads.set(r, payload = JSON.stringify([r.text, r.arguments, r.argumentsJSONPrefix, r.completeArgumentMembers]));
344
- return payload;
140
+ const analyses = new WeakMap<DialogueBlock, Analysis>();
141
+
142
+ /** The word budgets of a clipped excerpt: the passages kept from the start of the text, and from its end. */
143
+ export type Clip = readonly [headWords: number, tailWords: number];
144
+
145
+ /** The records of one dialogue block alone, before source times are attached: its whole original text, or, with a `clip`, the excerpts
146
+ * that keep their sanitized-block spans around an omission. A mandatory source renders nothing: the adapter quotes it in its base. */
147
+ export const blockRecords = (b: DialogueBlock, mandatory: ReadonlySet<number> | undefined, clip?: Clip): TranscriptRecord[] => {
148
+ if (!b.text.trim() || b.sourceIndex !== undefined && mandatory?.has(b.sourceIndex)) return [];
149
+ const role = b.kind === "notice" ? "custom" : b.kind;
150
+ const notice = b.kind === "notice" && b.sourceEntryId !== undefined ? { sourceEntryId: b.sourceEntryId } : {};
151
+ if (!clip) return [{ kind: "excerpt", role, ...(b.sourceIndex !== undefined ? { sourceIndex: b.sourceIndex } : {}), text: b.originalText ?? b.text, ...notice }];
152
+ let analysis = analyses.get(b);
153
+ if (!analysis) analyses.set(b, analysis = analyze(b.text));
154
+ // How to read what a clip leaves out. tuck_recall offsets count the source's readable text, so the clip's offset into the block is one
155
+ // only when the block begins that text and sanitizing left it as it was; otherwise the whole source is read from its start.
156
+ const locator = b.sourceIndex !== undefined ? { index: b.sourceIndex } : b.kind === "notice" && b.sourceEntryId !== undefined ? { entryId: b.sourceEntryId } : undefined;
157
+ const exact = b.startsSource === true && b.originalText === b.text;
158
+ const readFrom = (offset: number) => locator ? `tuck_recall ${JSON.stringify(exact ? { ...locator, offset } : locator)}` : "source has no locator to read it by";
159
+ return excerptRecords(b.text, analysis, role, b.sourceIndex, clip[0], clip[1], readFrom).map(record => ({ ...record, ...notice }));
345
160
  };
346
- /** Whether `record` is the same call as the `previous` record, shown a second time. */
347
- export const repeatsCall = (previous: TranscriptRecord | undefined, record: TranscriptRecord): previous is CallRecord & { sourceIndex: number } =>
348
- record.kind === "tool_call" && record.sourceIndex !== undefined && previous?.kind === "tool_call" &&
349
- previous.sourceIndex !== undefined && callPayload(previous) === callPayload(record);
350
- /** The earlier record of a repeated call, now also locating the later one. */
351
- export const absorbCall = (previous: TranscriptRecord, record: TranscriptRecord): TranscriptRecord =>
352
- ({ ...previous, sourceIndices: [...new Set([...(previous.sourceIndices ?? [previous.sourceIndex!]), ...(record.sourceIndices ?? [record.sourceIndex!])])].sort((a, b) => a - b) } as TranscriptRecord);
353
161
 
354
- /** A source time for each locator the record names; only briefs of times-carrying blocks have them. */
355
- export const noticeTimes = (blocks: NormalizedBlock[]): Map<string, SourceTime> =>
356
- new Map(blocks.flatMap(b => b.kind === "notice" && b.sourceEntryId && b.sourceTime ? [[b.sourceEntryId, b.sourceTime] as const] : []));
357
- export const decorateRecord = (record: TranscriptRecord, times: NormalizedBlock["sourceTimes"], entryTimes: ReadonlyMap<string, SourceTime>): TranscriptRecord => {
358
- if (!times) return record;
359
- if (record.sourceEntryId !== undefined) return { ...record, sourceTime: entryTimes.get(record.sourceEntryId) ?? { basis: "unknown" as const } };
162
+ /** A source time for each locator a user record names; only briefs of times-carrying blocks have them. */
163
+ export const decorateRecord = (record: TranscriptRecord, times: NormalizedBlock["sourceTimes"]): TranscriptRecord => {
164
+ // Times mark the user's turns only: on every record they cost a fifth to a third of the summary's tokens.
165
+ if (!times || record.role !== "user") return record;
360
166
  const indices = record.sourceIndices ?? (record.sourceIndex === undefined ? [] : [record.sourceIndex]);
361
167
  return indices.length ? { ...record, sourceTimes: indices.map(sourceIndex => ({ sourceIndex, ...(times[sourceIndex] ?? { basis: "unknown" as const }) })) } : record;
362
168
  };
363
-
364
- export const buildBriefRecords = (blocks: NormalizedBlock[], cache?: ExcerptCache): TranscriptRecord[] => {
365
- const records: TranscriptRecord[] = [];
366
- const coveredCalls = new Set(blocks.flatMap(b => receiptKey(b) ?? []));
367
- const plan = plans.get(blocks);
368
- for (let i = 0; i < blocks.length; i++) {
369
- const b = blocks[i];
370
- const key = coveredCallKey(b);
371
- const view = { full: Boolean(plan?.full.has(b)), closing: plan ? plan.closing.has(b) : isSegmentClosingAssistant(blocks, i),
372
- mandatory: plan?.mandatorySources, bounded: Boolean(plan), covered: key !== undefined && coveredCalls.has(key) };
373
- for (const record of blockRecords(b, view, cache)) {
374
- const previous = records.at(-1);
375
- if (repeatsCall(previous, record)) records[records.length - 1] = absorbCall(previous, record);
376
- else records.push(record);
377
- }
378
- }
379
- const times = blocks.find(b => b.sourceTimes)?.sourceTimes;
380
- const entryTimes = noticeTimes(blocks);
381
- return [...records, ...(plan?.omissions ?? [])].map(record => decorateRecord(record, times, entryTimes));
382
- };
383
-
384
- export const compileBrief = (blocks: NormalizedBlock[], cache?: ExcerptCache): string =>
385
- serializeRecords(buildBriefRecords(blocks, cache));
@@ -1,9 +1,10 @@
1
1
  import type { NormalizedBlock } from "../types";
2
2
  import { isSourceExport, isLookupTool, isRecallTool } from "./recall-operation";
3
+ import { isDialogue } from "./brief";
3
4
 
4
5
  /** Suppress lookup echoes, retaining recorded export attempts and exactly paired outcomes. */
5
6
  export const filterNoise = (blocks: NormalizedBlock[]): NormalizedBlock[] => blocks.filter(b =>
6
7
  !(b.kind === "tool_call" && isLookupTool(b.name) && !(isRecallTool(b.name) && isSourceExport(b.args))) &&
7
8
  !(b.kind === "tool_result" && isLookupTool(b.name) &&
8
9
  !(isRecallTool(b.name) && b.recallExport)) &&
9
- !((b.kind === "user" || b.kind === "assistant" || b.kind === "notice") && !b.text.trim()));
10
+ !(isDialogue(b) && !b.text.trim()));
@@ -1,13 +1,6 @@
1
1
  import { CLOSING_SEPARATOR } from "./transcript";
2
- export const RECALL_NOTE =
3
- "A tool_receipt record quotes complete original textual output. Its producer arguments are complete when present; argumentsOmitted means read the original call source for them. Use it directly as evidence of that recorded event; it does not verify current state or authorize replay. " +
4
- "Use `tuck_recall` to search for prior work, decisions, and context from before this summary. " +
5
- "Read call/receipt sources by index before relying on omitted or truncated evidence. Receipt non-error is not proof of effects. Do not redo work already completed.";
6
- const CLOSING_GUIDANCE = "Files created by commands are not listed: before redoing a step, read its output with tuck_recall or list your output folders. Re-read a file before editing it or importing from it. Use tuck_recall for anything else. Treat anything not shown here as unknown.";
7
- /** A dialogue summary's last words: it holds only the dialogue and the file ledger. */
8
- export const CLOSING_INSTRUCTION = "Only the dialogue and the actions and files listed above carry over. " + CLOSING_GUIDANCE;
9
- /** A ranked summary's last words: it also keeps selected tool records. */
10
- export const RANKED_CLOSING_INSTRUCTION = "Only the records and the actions and files listed above carry over. " + CLOSING_GUIDANCE;
11
- /** Each view's closing section, true for what that view holds. Parsing never depends on this wording (transcript.ts splitSummary). */
2
+ /** A summary's last words: it holds only the dialogue (whole or shrunk) and the file ledger. */
3
+ export const CLOSING_INSTRUCTION = "Only the dialogue and the actions and files listed above carry over. " +
4
+ "Files created by commands are not listed: before redoing a step, read its output with tuck_recall or list your output folders. Re-read a file before editing it or importing from it. Use tuck_recall for anything else. Treat anything not shown here as unknown.";
5
+ /** Parsing never depends on this wording (transcript.ts splitSummary). */
12
6
  export const DIALOGUE_SUFFIX = CLOSING_SEPARATOR + CLOSING_INSTRUCTION;
13
- export const RANKED_SUFFIX = CLOSING_SEPARATOR + RECALL_NOTE + "\n\n" + RANKED_CLOSING_INSTRUCTION;
@@ -1,15 +1,14 @@
1
1
  import { isSourceExport } from "./recall-operation";
2
- import type { CompilerMessage, NormalizedBlock, ReceiptLink, HistoricalReceipt } from "../types";
3
- import { isTextOnly, textOf } from "./content";
2
+ import type { CompilerMessage, NormalizedBlock } from "../types";
3
+ import { textOf } from "./content";
4
4
  import { sanitize } from "./sanitize";
5
- import { MAX_RECEIPT_CHARACTERS, smallOriginalArguments } from "./transcript";
6
5
 
7
- const normalizeOne = (msg: CompilerMessage, msgIndex: number | undefined, receipts: Map<string, ReceiptLink>, originals: Map<string, HistoricalReceipt>): NormalizedBlock[] => {
8
- if (msg.role === "custom") return [{ kind: "notice", text: sanitize(textOf(msg.content)), originalText: textOf(msg.content), sourceIndex: msgIndex }];
6
+ const normalizeOne = (msg: CompilerMessage, msgIndex: number | undefined): NormalizedBlock[] => {
7
+ if (msg.role === "custom") return [{ kind: "notice", text: sanitize(textOf(msg.content)), originalText: textOf(msg.content), sourceIndex: msgIndex, startsSource: true }];
9
8
  if (msg.role === "user") {
10
9
  const blocks: NormalizedBlock[] = [];
11
10
  const text = sanitize(textOf(msg.content));
12
- if (text) blocks.push({ kind: "user", text, originalText: textOf(msg.content), sourceIndex: msgIndex });
11
+ if (text) blocks.push({ kind: "user", text, originalText: textOf(msg.content), sourceIndex: msgIndex, startsSource: true });
13
12
  if (msg.content && typeof msg.content !== "string") {
14
13
  for (const part of msg.content) {
15
14
  if (part.type === "image") {
@@ -20,41 +19,29 @@ const normalizeOne = (msg: CompilerMessage, msgIndex: number | undefined, receip
20
19
  return blocks.length > 0 ? blocks : [{ kind: "user", text: "", sourceIndex: msgIndex }];
21
20
  }
22
21
 
23
- if (msg.role === "bashExecution") {
24
- const cmd = msg.command ?? "";
25
- const out = msg.output ?? "";
26
- const exit = msg.exitCode;
27
- return [{ kind: "bash", command: cmd, output: out, exitCode: exit, cancelled: msg.cancelled, truncated: msg.truncated, sourceIndex: msgIndex }];
28
- }
22
+ if (msg.role === "bashExecution") return [{ kind: "bash", sourceIndex: msgIndex }];
29
23
 
30
- if (msg.role === "toolResult") {
31
- const original = originals.get(msg.toolCallId);
32
- return [{
33
- kind: "tool_result",
34
- name: msg.toolName,
35
- text: sanitize(textOf(msg.content)),
36
- sourceIndex: msgIndex,
37
- originalReceipt: (original?.text.length ?? Infinity) <= MAX_RECEIPT_CHARACTERS ? original : undefined,
38
- }];
39
- }
24
+ if (msg.role === "toolResult") return [{ kind: "tool_result", name: msg.toolName, sourceIndex: msgIndex }];
40
25
 
41
26
  if (msg.role === "assistant") {
42
27
  if (!msg.content) return [];
43
28
  if (typeof msg.content === "string") {
44
- return [{ kind: "assistant", text: sanitize(msg.content), originalText: msg.content, sourceIndex: msgIndex }];
29
+ return [{ kind: "assistant", text: sanitize(msg.content), originalText: msg.content, sourceIndex: msgIndex, startsSource: true }];
45
30
  }
46
31
 
47
32
  const blocks: NormalizedBlock[] = [];
33
+ // The readable text of a message is its text and tool-call parts in order: only the first part starts it.
34
+ let readable = false;
48
35
  for (const part of msg.content) {
49
36
  if (part.type === "text") {
50
- blocks.push({ kind: "assistant", text: sanitize(part.text), originalText: part.text, sourceIndex: msgIndex });
37
+ blocks.push({ kind: "assistant", text: sanitize(part.text), originalText: part.text, sourceIndex: msgIndex, ...(readable ? {} : { startsSource: true as const }) });
38
+ readable = true;
51
39
  } else if (part.type === "toolCall") {
40
+ readable = true;
52
41
  blocks.push({
53
42
  kind: "tool_call",
54
43
  name: part.name,
55
44
  args: part.arguments,
56
- callId: part.id,
57
- receipt: receipts.get(part.id),
58
45
  sourceIndex: msgIndex,
59
46
  });
60
47
  }
@@ -103,30 +90,14 @@ export const normalize = (
103
90
  order, index: context.sourceIndices[order], isError: msg.isError, content: msg.content });
104
91
  }
105
92
  });
106
- const links = new Map<string, ReceiptLink>();
107
- const originals = new Map<string, HistoricalReceipt>();
108
- for (const [id, { calls, receipts }] of pairs) {
109
- const call = calls[0], receipt = receipts[0];
110
- const status = calls.length !== 1 || receipts.length > 1 ? "ambiguous" : !receipt ? "missing" :
111
- receipt.name !== call.name || receipt.order <= call.order ? "ambiguous" : receipt.isError ? "error" : "non-error";
112
- links.set(id, { status, sourceIndices: receipts.flatMap(r => r.index === undefined ? [] : [r.index]),
113
- dependencyIndices: [...calls, ...receipts].flatMap(source => source.index === undefined ? [] : [source.index]) });
114
- if ((status === "error" || status === "non-error") && call.index !== undefined && receipt.index !== undefined &&
115
- Number.isSafeInteger(call.index) && call.index >= 0 && Number.isSafeInteger(receipt.index) && receipt.index > call.index &&
116
- call.name.trim() && call.name.length <= 160 && id.length <= 160 && smallOriginalArguments(call.args, Number.MAX_SAFE_INTEGER) &&
117
- isTextOnly(receipt.content)) {
118
- const text = textOf(receipt.content);
119
- if (text) originals.set(id, {
120
- text, isError: receipt.isError,
121
- producer: { sourceIndex: call.index, toolName: call.name, toolCallId: id,
122
- ...(smallOriginalArguments(call.args) ? { arguments: call.args } : { argumentsOmitted: true as const }) },
123
- });
124
- }
125
- }
126
- return messages.flatMap((msg, i) => normalizeOne(msg, sourceIndices ? sourceIndices[i] : i, links, originals).map(block => {
127
- if (block.kind === "tool_result" && msg.role === "toolResult" &&
128
- ["error", "non-error"].includes(links.get(msg.toolCallId)?.status ?? "") &&
93
+ // A call paired with exactly one later receipt of the same tool has a recorded outcome. An export through recall keeps its outcome block.
94
+ const paired = (id: string): boolean => {
95
+ const pair = pairs.get(id), call = pair?.calls[0], receipt = pair?.receipts[0];
96
+ return Boolean(pair && call && receipt && pair.calls.length === 1 && pair.receipts.length === 1 && receipt.name === call.name && receipt.order > call.order);
97
+ };
98
+ return messages.flatMap((msg, i) => normalizeOne(msg, sourceIndices ? sourceIndices[i] : i).map(block => {
99
+ if (block.kind === "tool_result" && msg.role === "toolResult" && paired(msg.toolCallId) &&
129
100
  isSourceExport(pairs.get(msg.toolCallId)?.calls[0]?.args)) block = { ...block, recallExport: true };
130
- if (sourceTimes) block = { ...block, sourceTime: sourceTimes[i], sourceTimes: timeByIndex };
101
+ if (sourceTimes) block = { ...block, sourceTimes: timeByIndex };
131
102
  return block.kind === "notice" && sourceEntryIds?.[i] !== undefined ? { ...block, sourceEntryId: sourceEntryIds[i] } : block; }));
132
103
  };