supercompact 1.0.0
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/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/cli.js +616 -0
- package/dist/dialogue.js +93 -0
- package/dist/estimate.js +217 -0
- package/dist/keep.js +76 -0
- package/dist/measure.js +204 -0
- package/dist/preview.js +163 -0
- package/dist/rewrite.js +342 -0
- package/dist/store.js +261 -0
- package/dist/transcript.js +182 -0
- package/package.json +34 -0
package/dist/preview.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// What a rewrite would cost, worked out without writing anything.
|
|
2
|
+
//
|
|
3
|
+
// A menu that asks this on every click cannot wait for the file to be rebuilt
|
|
4
|
+
// each time, so nothing is rebuilt. One pass prices each option on its own and
|
|
5
|
+
// the caller adds up whichever ones are switched on.
|
|
6
|
+
import { promptText, signature, stripNoise, summarize } from './dialogue.js';
|
|
7
|
+
import { tokensInContent, tokensInText } from './estimate.js';
|
|
8
|
+
import { isRecord } from './transcript.js';
|
|
9
|
+
export const messageLadder = [2, 5, 10, 20, 40];
|
|
10
|
+
export const toolLadder = [1, 3, 5, 10, 20];
|
|
11
|
+
export function after(preview, withToolLines, keep) {
|
|
12
|
+
let total = preview.dialogue;
|
|
13
|
+
if (withToolLines)
|
|
14
|
+
total += preview.toolLines;
|
|
15
|
+
total += costAt(preview.lastMessages, keep.lastMessages);
|
|
16
|
+
total += costAt(keep.unique ? preview.uniqueCalls : preview.toolCalls, keep.toolCalls);
|
|
17
|
+
return total;
|
|
18
|
+
}
|
|
19
|
+
/** The N a caller asks for may sit between two rungs. The rung at or below it
|
|
20
|
+
* is the honest answer. */
|
|
21
|
+
export function costAt(steps, n) {
|
|
22
|
+
if (n <= 0 || steps.length === 0)
|
|
23
|
+
return 0;
|
|
24
|
+
let best = 0;
|
|
25
|
+
for (const step of steps)
|
|
26
|
+
if (step.n <= n)
|
|
27
|
+
best = step.tokens;
|
|
28
|
+
return best === 0 ? (steps[0]?.tokens ?? 0) : best;
|
|
29
|
+
}
|
|
30
|
+
export function buildPreview(transcript) {
|
|
31
|
+
const preview = {
|
|
32
|
+
now: 0,
|
|
33
|
+
nowReported: false,
|
|
34
|
+
bytes: transcript.bytes,
|
|
35
|
+
dialogue: 0,
|
|
36
|
+
toolLines: 0,
|
|
37
|
+
lastMessages: [],
|
|
38
|
+
toolCalls: [],
|
|
39
|
+
uniqueCalls: [],
|
|
40
|
+
messages: 0,
|
|
41
|
+
calls: 0,
|
|
42
|
+
};
|
|
43
|
+
const calls = [];
|
|
44
|
+
const answers = new Map();
|
|
45
|
+
const messages = [];
|
|
46
|
+
const extras = new Map();
|
|
47
|
+
let reported = 0;
|
|
48
|
+
const addExtra = (index, tokens) => {
|
|
49
|
+
extras.set(index, (extras.get(index) ?? 0) + tokens);
|
|
50
|
+
};
|
|
51
|
+
for (const entry of transcript.entries) {
|
|
52
|
+
const message = entry.message;
|
|
53
|
+
if (message === undefined)
|
|
54
|
+
continue;
|
|
55
|
+
if (entry.type === 'assistant' && entry.data.usageIsEstimate !== true) {
|
|
56
|
+
const usage = message.usage;
|
|
57
|
+
if (isRecord(usage)) {
|
|
58
|
+
const held = num(usage.input_tokens) +
|
|
59
|
+
num(usage.cache_read_input_tokens) +
|
|
60
|
+
num(usage.cache_creation_input_tokens);
|
|
61
|
+
if (held > 0)
|
|
62
|
+
reported = held;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (entry.type === 'user') {
|
|
66
|
+
const text = promptText(entry.content);
|
|
67
|
+
if (text !== undefined) {
|
|
68
|
+
preview.dialogue += tokensInText(stripNoise(text));
|
|
69
|
+
// An image pasted into a prompt is dropped by a plain rewrite and kept
|
|
70
|
+
// by the window, so it is priced with the window, not the dialogue.
|
|
71
|
+
const pasted = entry.blocks.filter((block) => block.type === 'image').length;
|
|
72
|
+
if (pasted > 0)
|
|
73
|
+
addExtra(entry.index, pasted * 1500);
|
|
74
|
+
messages.push(entry.index);
|
|
75
|
+
preview.messages++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
for (const block of entry.blocks) {
|
|
79
|
+
if (block.type !== 'tool_result')
|
|
80
|
+
continue;
|
|
81
|
+
const id = typeof block.tool_use_id === 'string' ? block.tool_use_id : '';
|
|
82
|
+
const tokens = tokensInContent([block]);
|
|
83
|
+
answers.set(id, tokens);
|
|
84
|
+
addExtra(entry.index, tokens);
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (entry.type !== 'assistant')
|
|
89
|
+
continue;
|
|
90
|
+
messages.push(entry.index);
|
|
91
|
+
preview.messages++;
|
|
92
|
+
for (const block of entry.blocks) {
|
|
93
|
+
if (block.type === 'text') {
|
|
94
|
+
preview.dialogue += tokensInText(typeof block.text === 'string' ? block.text : '');
|
|
95
|
+
}
|
|
96
|
+
else if (block.type === 'tool_use') {
|
|
97
|
+
const id = typeof block.id === 'string' ? block.id : '';
|
|
98
|
+
if (id === '')
|
|
99
|
+
continue;
|
|
100
|
+
preview.toolLines += tokensInText(summarize(block));
|
|
101
|
+
const tokens = tokensInContent([block]);
|
|
102
|
+
preview.calls++;
|
|
103
|
+
calls.push({ id, sig: signature(block), tokens });
|
|
104
|
+
addExtra(entry.index, tokens);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (reported > 0) {
|
|
109
|
+
preview.now = reported;
|
|
110
|
+
preview.nowReported = true;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
let total = preview.dialogue + preview.toolLines;
|
|
114
|
+
for (const tokens of extras.values())
|
|
115
|
+
total += tokens;
|
|
116
|
+
preview.now = total;
|
|
117
|
+
}
|
|
118
|
+
preview.lastMessages = messageLadder.map((n) => {
|
|
119
|
+
const start = messages[Math.max(0, messages.length - n)];
|
|
120
|
+
let tokens = 0;
|
|
121
|
+
if (start !== undefined) {
|
|
122
|
+
for (const [index, value] of extras)
|
|
123
|
+
if (index >= start)
|
|
124
|
+
tokens += value;
|
|
125
|
+
}
|
|
126
|
+
return { n, tokens };
|
|
127
|
+
});
|
|
128
|
+
const newestFirst = [...calls].reverse();
|
|
129
|
+
preview.toolCalls = ladder(newestFirst, answers, false);
|
|
130
|
+
preview.uniqueCalls = ladder(newestFirst, answers, true);
|
|
131
|
+
return preview;
|
|
132
|
+
}
|
|
133
|
+
function ladder(newestFirst, answers, unique) {
|
|
134
|
+
let running = 0;
|
|
135
|
+
let taken = 0;
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const byCount = new Map();
|
|
138
|
+
const ceiling = Math.max(...toolLadder);
|
|
139
|
+
for (const call of newestFirst) {
|
|
140
|
+
const answer = answers.get(call.id);
|
|
141
|
+
if (answer === undefined)
|
|
142
|
+
continue;
|
|
143
|
+
if (unique) {
|
|
144
|
+
if (seen.has(call.sig))
|
|
145
|
+
continue;
|
|
146
|
+
seen.add(call.sig);
|
|
147
|
+
}
|
|
148
|
+
running += answer + call.tokens;
|
|
149
|
+
taken++;
|
|
150
|
+
if (toolLadder.includes(taken))
|
|
151
|
+
byCount.set(taken, running);
|
|
152
|
+
if (taken >= ceiling)
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
let last = 0;
|
|
156
|
+
return toolLadder.map((n) => {
|
|
157
|
+
last = byCount.get(n) ?? last;
|
|
158
|
+
return { n, tokens: last };
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function num(value) {
|
|
162
|
+
return typeof value === 'number' ? value : 0;
|
|
163
|
+
}
|
package/dist/rewrite.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { decode, encode, isRecord } from './transcript.js';
|
|
3
|
+
import { promptText, stripNoise, summarize } from './dialogue.js';
|
|
4
|
+
import { inWindow, keepIsEmpty, noKeep, plan } from './keep.js';
|
|
5
|
+
export const defaultOptions = { toolLines: false, keep: noKeep };
|
|
6
|
+
function emptyResult(sessionId = '') {
|
|
7
|
+
return {
|
|
8
|
+
jsonl: '',
|
|
9
|
+
sessionId,
|
|
10
|
+
users: 0,
|
|
11
|
+
assistants: 0,
|
|
12
|
+
toolLines: 0,
|
|
13
|
+
keptCalls: 0,
|
|
14
|
+
dropped: 0,
|
|
15
|
+
preservedTail: 0,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function messageCount(result) {
|
|
19
|
+
return result.users + result.assistants;
|
|
20
|
+
}
|
|
21
|
+
/** Collects the rewritten lines and keeps the parent chain honest. */
|
|
22
|
+
class Builder {
|
|
23
|
+
result;
|
|
24
|
+
lines = [];
|
|
25
|
+
parent = null;
|
|
26
|
+
written = new Set();
|
|
27
|
+
notes = [];
|
|
28
|
+
constructor(result) {
|
|
29
|
+
this.result = result;
|
|
30
|
+
}
|
|
31
|
+
emit(record, uuid) {
|
|
32
|
+
this.lines.push(encode(record));
|
|
33
|
+
if (uuid !== '') {
|
|
34
|
+
this.parent = uuid;
|
|
35
|
+
this.written.add(uuid);
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
/** Notes about dropped calls ride at the end of a message that is already
|
|
40
|
+
* going out. That is the only safe place for them when a call was kept: the
|
|
41
|
+
* API reads the message straight after a tool_use and wants the answer. */
|
|
42
|
+
attachNotes(record) {
|
|
43
|
+
if (this.notes.length === 0)
|
|
44
|
+
return false;
|
|
45
|
+
const message = record.message;
|
|
46
|
+
if (!isRecord(message) || !Array.isArray(message.content))
|
|
47
|
+
return false;
|
|
48
|
+
message.content = [...message.content, { type: 'text', text: this.notes.join('\n') }];
|
|
49
|
+
record.message = message;
|
|
50
|
+
this.result.toolLines += this.notes.length;
|
|
51
|
+
this.notes = [];
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/** Notes as their own turn. Safe only when no kept call is waiting. */
|
|
55
|
+
flushNotes(source, sessionId) {
|
|
56
|
+
if (this.notes.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
const uuid = randomUUID();
|
|
59
|
+
const record = {
|
|
60
|
+
type: 'user',
|
|
61
|
+
uuid,
|
|
62
|
+
parentUuid: this.parent,
|
|
63
|
+
message: { role: 'user', content: this.notes.join('\n') },
|
|
64
|
+
};
|
|
65
|
+
for (const key of ['sessionId', 'timestamp', 'cwd', 'gitBranch', 'version', 'userType']) {
|
|
66
|
+
if (source[key] !== undefined)
|
|
67
|
+
record[key] = source[key];
|
|
68
|
+
}
|
|
69
|
+
if (sessionId !== '')
|
|
70
|
+
record.sessionId = sessionId;
|
|
71
|
+
this.emit(record, uuid);
|
|
72
|
+
this.result.toolLines += this.notes.length;
|
|
73
|
+
this.notes = [];
|
|
74
|
+
}
|
|
75
|
+
finish() {
|
|
76
|
+
if (this.lines.length > 0)
|
|
77
|
+
this.result.jsonl = this.lines.join('\n') + '\n';
|
|
78
|
+
return this.result;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Separates what an assistant turn said from the calls it made, keeping any
|
|
82
|
+
* call the selection asked for. */
|
|
83
|
+
function splitAssistant(entry, selection, toolLines) {
|
|
84
|
+
const blocks = [];
|
|
85
|
+
const notes = [];
|
|
86
|
+
let keptCall = false;
|
|
87
|
+
for (const block of entry.blocks) {
|
|
88
|
+
if (block.type === 'text') {
|
|
89
|
+
const text = typeof block.text === 'string' ? block.text : '';
|
|
90
|
+
if (text.trim() !== '')
|
|
91
|
+
blocks.push(block);
|
|
92
|
+
}
|
|
93
|
+
else if (block.type === 'tool_use') {
|
|
94
|
+
const id = typeof block.id === 'string' ? block.id : '';
|
|
95
|
+
if (selection.calls.has(id)) {
|
|
96
|
+
blocks.push(block);
|
|
97
|
+
keptCall = true;
|
|
98
|
+
}
|
|
99
|
+
else if (toolLines) {
|
|
100
|
+
const note = summarize(block);
|
|
101
|
+
if (note !== '')
|
|
102
|
+
notes.push(note);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { blocks, notes, keptCall };
|
|
107
|
+
}
|
|
108
|
+
/** A turn of tool results, trimmed to the ones chosen to survive. */
|
|
109
|
+
function keptResults(entry, selection) {
|
|
110
|
+
if (selection.calls.size === 0)
|
|
111
|
+
return [];
|
|
112
|
+
return entry.blocks.filter((block) => block.type === 'tool_result' &&
|
|
113
|
+
typeof block.tool_use_id === 'string' &&
|
|
114
|
+
selection.calls.has(block.tool_use_id));
|
|
115
|
+
}
|
|
116
|
+
/** Builds a new session from an old one: the words both sides said, and nothing
|
|
117
|
+
* else unless it was asked for.
|
|
118
|
+
*
|
|
119
|
+
* Every entry gets a fresh uuid and the chain is rebuilt in order, so the
|
|
120
|
+
* result stands on its own and the original is never opened for writing. */
|
|
121
|
+
export function fork(transcript, options = defaultOptions) {
|
|
122
|
+
const selection = plan(transcript, options.keep);
|
|
123
|
+
const sessionId = randomUUID();
|
|
124
|
+
const b = new Builder(emptyResult(sessionId));
|
|
125
|
+
for (const entry of transcript.entries) {
|
|
126
|
+
if (entry.type !== 'user' && entry.type !== 'assistant')
|
|
127
|
+
continue;
|
|
128
|
+
if (entry.isSidechain)
|
|
129
|
+
continue;
|
|
130
|
+
if (inWindow(selection, entry.index)) {
|
|
131
|
+
const record = { ...entry.data };
|
|
132
|
+
const uuid = randomUUID();
|
|
133
|
+
record.uuid = uuid;
|
|
134
|
+
record.parentUuid = b.parent;
|
|
135
|
+
record.sessionId = sessionId;
|
|
136
|
+
const message = isRecord(record.message) ? { ...record.message } : undefined;
|
|
137
|
+
if (message) {
|
|
138
|
+
delete message.usage;
|
|
139
|
+
record.message = message;
|
|
140
|
+
}
|
|
141
|
+
b.attachNotes(record);
|
|
142
|
+
b.emit(record, uuid);
|
|
143
|
+
if (entry.type === 'assistant')
|
|
144
|
+
b.result.assistants++;
|
|
145
|
+
else if (promptText(entry.content) !== undefined)
|
|
146
|
+
b.result.users++;
|
|
147
|
+
else
|
|
148
|
+
b.result.keptCalls++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (entry.type === 'user') {
|
|
152
|
+
const kept = keptResults(entry, selection);
|
|
153
|
+
if (kept.length > 0) {
|
|
154
|
+
const record = { ...entry.data };
|
|
155
|
+
const uuid = randomUUID();
|
|
156
|
+
record.uuid = uuid;
|
|
157
|
+
record.parentUuid = b.parent;
|
|
158
|
+
record.sessionId = sessionId;
|
|
159
|
+
record.message = { ...(entry.message ?? {}), content: kept };
|
|
160
|
+
b.attachNotes(record);
|
|
161
|
+
b.emit(record, uuid);
|
|
162
|
+
b.result.keptCalls += kept.length;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
b.flushNotes(entry.data, sessionId);
|
|
166
|
+
if (entry.isCompactSummary || entry.isTranscriptOnly)
|
|
167
|
+
continue;
|
|
168
|
+
const raw = promptText(entry.content);
|
|
169
|
+
if (raw === undefined)
|
|
170
|
+
continue;
|
|
171
|
+
const text = stripNoise(raw);
|
|
172
|
+
if (text === '')
|
|
173
|
+
continue;
|
|
174
|
+
const record = { ...entry.data };
|
|
175
|
+
const uuid = randomUUID();
|
|
176
|
+
record.uuid = uuid;
|
|
177
|
+
record.parentUuid = b.parent;
|
|
178
|
+
record.sessionId = sessionId;
|
|
179
|
+
record.message = { ...(entry.message ?? {}), role: 'user', content: text };
|
|
180
|
+
b.emit(record, uuid);
|
|
181
|
+
b.result.users++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
b.flushNotes(entry.data, sessionId);
|
|
185
|
+
if (entry.isApiError)
|
|
186
|
+
continue;
|
|
187
|
+
const { blocks, notes, keptCall } = splitAssistant(entry, selection, options.toolLines);
|
|
188
|
+
if (blocks.length === 0) {
|
|
189
|
+
b.notes = notes;
|
|
190
|
+
b.flushNotes(entry.data, sessionId);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const record = { ...entry.data };
|
|
194
|
+
const uuid = randomUUID();
|
|
195
|
+
record.uuid = uuid;
|
|
196
|
+
record.parentUuid = b.parent;
|
|
197
|
+
record.sessionId = sessionId;
|
|
198
|
+
const message = { ...(entry.message ?? {}), content: blocks };
|
|
199
|
+
if (!keptCall && message.stop_reason === 'tool_use')
|
|
200
|
+
message.stop_reason = 'end_turn';
|
|
201
|
+
delete message.usage;
|
|
202
|
+
delete message.id;
|
|
203
|
+
delete message.stop_details;
|
|
204
|
+
delete record.requestId;
|
|
205
|
+
record.message = message;
|
|
206
|
+
b.emit(record, uuid);
|
|
207
|
+
b.result.assistants++;
|
|
208
|
+
b.notes = notes;
|
|
209
|
+
if (!keptCall)
|
|
210
|
+
b.flushNotes(entry.data, sessionId);
|
|
211
|
+
}
|
|
212
|
+
return b.finish();
|
|
213
|
+
}
|
|
214
|
+
/** Rewrites a session's own file, so it keeps its id and its name.
|
|
215
|
+
*
|
|
216
|
+
* A running Claude Code process holds the end of this file in memory and will
|
|
217
|
+
* append to it, chaining whatever it writes next to the last uuid it saw. So
|
|
218
|
+
* the turn in progress goes back untouched and every entry that survives keeps
|
|
219
|
+
* its original uuid. Break that and the next line written points at an entry
|
|
220
|
+
* that no longer exists, which collapses the history on resume. */
|
|
221
|
+
export function inPlace(transcript, options = defaultOptions) {
|
|
222
|
+
const selection = plan(transcript, options.keep);
|
|
223
|
+
let tailStart = tailBoundary(transcript);
|
|
224
|
+
if (selection.from !== undefined && selection.from < tailStart)
|
|
225
|
+
tailStart = selection.from;
|
|
226
|
+
const b = new Builder(emptyResult(transcript.id));
|
|
227
|
+
for (const entry of transcript.entries) {
|
|
228
|
+
if (entry.index >= tailStart) {
|
|
229
|
+
b.result.preservedTail++;
|
|
230
|
+
const record = decode(entry.raw);
|
|
231
|
+
if (record === undefined) {
|
|
232
|
+
b.lines.push(entry.raw);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// A rewound conversation branches, so an entry near the end can be the
|
|
236
|
+
// child of something far earlier. Any parent that did not survive is
|
|
237
|
+
// re-pointed, not just the first one.
|
|
238
|
+
const parent = record.parentUuid;
|
|
239
|
+
if (typeof parent !== 'string' || !b.written.has(parent))
|
|
240
|
+
record.parentUuid = b.parent;
|
|
241
|
+
b.attachNotes(record);
|
|
242
|
+
stripUsage(record);
|
|
243
|
+
b.emit(record, typeof record.uuid === 'string' ? record.uuid : '');
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (entry.type === 'user') {
|
|
247
|
+
const kept = keptResults(entry, selection);
|
|
248
|
+
if (kept.length > 0 && entry.uuid !== '') {
|
|
249
|
+
const record = { ...entry.data };
|
|
250
|
+
record.parentUuid = b.parent;
|
|
251
|
+
record.message = { ...(entry.message ?? {}), content: kept };
|
|
252
|
+
b.attachNotes(record);
|
|
253
|
+
b.emit(record, entry.uuid);
|
|
254
|
+
b.result.keptCalls += kept.length;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
b.flushNotes(entry.data, '');
|
|
258
|
+
if (entry.isCompactSummary || entry.isTranscriptOnly || entry.uuid === '') {
|
|
259
|
+
b.result.dropped++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const raw = promptText(entry.content);
|
|
263
|
+
if (raw === undefined) {
|
|
264
|
+
b.result.dropped++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const text = stripNoise(raw);
|
|
268
|
+
if (text === '') {
|
|
269
|
+
b.result.dropped++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const record = { ...entry.data };
|
|
273
|
+
record.parentUuid = b.parent;
|
|
274
|
+
record.message = { ...(entry.message ?? {}), content: text };
|
|
275
|
+
b.emit(record, entry.uuid);
|
|
276
|
+
b.result.users++;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (entry.type !== 'assistant') {
|
|
280
|
+
b.result.dropped++;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
b.flushNotes(entry.data, '');
|
|
284
|
+
if (entry.isApiError || entry.uuid === '') {
|
|
285
|
+
b.result.dropped++;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const { blocks, notes, keptCall } = splitAssistant(entry, selection, options.toolLines);
|
|
289
|
+
if (blocks.length === 0) {
|
|
290
|
+
b.result.dropped++;
|
|
291
|
+
b.notes = notes;
|
|
292
|
+
b.flushNotes(entry.data, '');
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const record = { ...entry.data };
|
|
296
|
+
record.parentUuid = b.parent;
|
|
297
|
+
const message = { ...(entry.message ?? {}), content: blocks };
|
|
298
|
+
if (!keptCall && message.stop_reason === 'tool_use')
|
|
299
|
+
message.stop_reason = 'end_turn';
|
|
300
|
+
delete message.usage;
|
|
301
|
+
delete message.stop_details;
|
|
302
|
+
delete record.requestId;
|
|
303
|
+
record.message = message;
|
|
304
|
+
b.emit(record, entry.uuid);
|
|
305
|
+
b.result.assistants++;
|
|
306
|
+
b.notes = notes;
|
|
307
|
+
if (!keptCall)
|
|
308
|
+
b.flushNotes(entry.data, '');
|
|
309
|
+
}
|
|
310
|
+
return b.finish();
|
|
311
|
+
}
|
|
312
|
+
/** The last thing the person typed. Everything after it belongs to the turn the
|
|
313
|
+
* caller is standing in and goes back untouched.
|
|
314
|
+
*
|
|
315
|
+
* A prompt carrying a pasted image is stored as blocks rather than a string.
|
|
316
|
+
* Miss that and the boundary slides back to the last plain-text prompt, which
|
|
317
|
+
* can drag an entire unattended run of screenshots into the preserved part. */
|
|
318
|
+
function tailBoundary(transcript) {
|
|
319
|
+
for (let i = transcript.entries.length - 1; i >= 0; i--) {
|
|
320
|
+
const entry = transcript.entries[i];
|
|
321
|
+
if (entry.type !== 'user' || entry.isCompactSummary || entry.isTranscriptOnly)
|
|
322
|
+
continue;
|
|
323
|
+
if (promptText(entry.content) !== undefined)
|
|
324
|
+
return entry.index;
|
|
325
|
+
}
|
|
326
|
+
return Math.max(0, transcript.entries.length - 1);
|
|
327
|
+
}
|
|
328
|
+
/** The preserved tail keeps its uuids, which is what lets a live session go on
|
|
329
|
+
* writing, but its usage blocks describe the session as it was before the
|
|
330
|
+
* rewrite. Left alone they report the old size forever. */
|
|
331
|
+
function stripUsage(record) {
|
|
332
|
+
if (record.type !== 'assistant')
|
|
333
|
+
return;
|
|
334
|
+
const message = record.message;
|
|
335
|
+
if (!isRecord(message))
|
|
336
|
+
return;
|
|
337
|
+
delete message.usage;
|
|
338
|
+
delete message.stop_details;
|
|
339
|
+
record.message = message;
|
|
340
|
+
delete record.requestId;
|
|
341
|
+
}
|
|
342
|
+
export { keepIsEmpty };
|