@bli-cockpit/cli 0.2.263 → 0.2.265

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.
@@ -22,6 +22,7 @@
22
22
  * What each rule keeps and drops: claude-rules.ts and codex-rules.ts. The table for people:
23
23
  * docs/architecture/image-descriptions.md.
24
24
  */
25
+ import { replaceImagesWithMarkers } from "@bli-cockpit/telemetry-core";
25
26
  import { newClaudeSlimState, slimClaudeRecord } from "./claude-rules.js";
26
27
  import { newCodexSlimState, slimCodexRecord } from "./codex-rules.js";
27
28
  import { LineEdits, SeenBlocks, isJsonObject } from "./slim-support.js";
@@ -103,17 +104,89 @@ function slimOneLine(line, applyRules, stats) {
103
104
  stats.linesModified += 1;
104
105
  return JSON.stringify(record) + (line.endsWith("\r") ? "\r" : "");
105
106
  }
106
- /** The slim form of a whole transcript's text, line for line. */
107
- export function slimTranscript(text, kind) {
107
+ /**
108
+ * The slim form of a whole transcript's text, line for line, with the image swap folded into the
109
+ * same pass (BLI-23317). Output is byte-identical to `replaceImagesWithMarkers` followed by the old
110
+ * split/map/join: lines are found by `indexOf`, never `split`; a line no rule touched stays part of
111
+ * a slice of the input and is not copied on its own, so the only new allocation is the one joined
112
+ * result (and only when some line changed). An image sits inside one line (its characters exclude
113
+ * "\n"), so it is swapped inside that line before the rules read it, as the two-step order did.
114
+ */
115
+ export function slimPass(text, kind, swap) {
108
116
  const slimmer = createSlimmer(kind);
109
- const lines = text.split("\n");
110
- const out = new Array(lines.length);
111
- for (let index = 0; index < lines.length; index += 1)
112
- out[index] = slimmer.slimLine(lines[index]);
117
+ const pieces = [];
118
+ let emitted = 0;
119
+ let nextImage = 0;
120
+ let replacedCount = 0;
121
+ let charsSaved = 0;
122
+ for (let position = 0;;) {
123
+ const newline = text.indexOf("\n", position);
124
+ const end = newline === -1 ? text.length : newline;
125
+ let line = text.slice(position, end);
126
+ let swapped = false;
127
+ if (swap) {
128
+ const local = [];
129
+ while (nextImage < swap.images.length && swap.images[nextImage].replaceStart < end) {
130
+ const image = swap.images[nextImage];
131
+ nextImage += 1;
132
+ if (image.replaceStart < position || image.replaceEnd > end)
133
+ throw new Error("image_spans_lines");
134
+ local.push({
135
+ ...image,
136
+ replaceStart: image.replaceStart - position,
137
+ replaceEnd: image.replaceEnd - position,
138
+ base64Start: image.base64Start - position,
139
+ base64End: image.base64End - position,
140
+ });
141
+ }
142
+ if (local.length > 0) {
143
+ const replaced = replaceImagesWithMarkers(line, local, swap.replace);
144
+ if (replaced.replacedCount > 0) {
145
+ line = replaced.text;
146
+ swapped = true;
147
+ replacedCount += replaced.replacedCount;
148
+ charsSaved += replaced.charsSaved;
149
+ }
150
+ }
151
+ }
152
+ const out = slimmer.slimLine(line);
153
+ if (swapped || out !== line) {
154
+ pieces.push(text.slice(emitted, position), out);
155
+ emitted = end;
156
+ }
157
+ if (newline === -1)
158
+ break;
159
+ position = newline + 1;
160
+ }
113
161
  const stats = slimmer.stats();
114
- return { text: stats.linesModified === 0 ? text : out.join("\n"), stats };
162
+ if (pieces.length === 0)
163
+ return { text, stats, replacedCount, charsSaved, changed: false };
164
+ pieces.push(text.slice(emitted));
165
+ return { text: pieces.join(""), stats, replacedCount, charsSaved, changed: true };
166
+ }
167
+ /** The slim form of a whole transcript's text, line for line. */
168
+ export function slimTranscript(text, kind) {
169
+ const slim = slimPass(text, kind);
170
+ return { text: slim.text, stats: slim.stats };
115
171
  }
116
172
  const TAG = "[slim-transcript]";
173
+ export function logSlimmed(kind, sessionId, stats, startedAt) {
174
+ console.error(TAG + (stats.linesModified === 0 ? " nothing to strip" : " transcript slimmed"), JSON.stringify({
175
+ source: kind,
176
+ session_id: sessionId.slice(0, 80),
177
+ lines: stats.lines,
178
+ lines_modified: stats.linesModified,
179
+ lines_failed: stats.linesFailed,
180
+ bytes_before: stats.bytesBefore,
181
+ bytes_after: stats.bytesAfter,
182
+ chars_by_rule: stats.charsByRule,
183
+ lines_by_rule: stats.linesByRule,
184
+ elapsed_ms: Date.now() - startedAt,
185
+ }));
186
+ }
187
+ function logSlimFailed(kind, error, bytes) {
188
+ console.error(TAG + " failed, uploading the transcript as it was", JSON.stringify({ reason: "slim_failed", source: kind, error_name: error instanceof Error ? error.name : typeof error, bytes }));
189
+ }
117
190
  /**
118
191
  * `slimTranscript` that can never fail a transcript: any exception returns the text it was given and
119
192
  * logs the reason. Metadata only: counts, sizes, rule names. Never a line, never a field value.
@@ -122,23 +195,27 @@ export function slimOrKeep(text, kind, label) {
122
195
  const startedAt = Date.now();
123
196
  try {
124
197
  const slim = slimTranscript(text, kind);
125
- const stats = slim.stats;
126
- console.error(TAG + (stats.linesModified === 0 ? " nothing to strip" : " transcript slimmed"), JSON.stringify({
127
- source: kind,
128
- session_id: label.sessionId.slice(0, 80),
129
- lines: stats.lines,
130
- lines_modified: stats.linesModified,
131
- lines_failed: stats.linesFailed,
132
- bytes_before: stats.bytesBefore,
133
- bytes_after: stats.bytesAfter,
134
- chars_by_rule: stats.charsByRule,
135
- lines_by_rule: stats.linesByRule,
136
- elapsed_ms: Date.now() - startedAt,
137
- }));
138
- return { text: slim.text, stats };
198
+ logSlimmed(kind, label.sessionId, slim.stats, startedAt);
199
+ return { text: slim.text, stats: slim.stats };
139
200
  }
140
201
  catch (error) {
141
- console.error(TAG + " failed, uploading the transcript as it was", JSON.stringify({ reason: "slim_failed", source: kind, error_name: error instanceof Error ? error.name : typeof error, bytes: typeof text === "string" ? text.length : 0 }));
202
+ logSlimFailed(kind, error, typeof text === "string" ? text.length : 0);
142
203
  return { text, stats: null };
143
204
  }
205
+ }
206
+ /**
207
+ * `slimPass` that can never fail a transcript: a pass that throws (an image that would span lines,
208
+ * an exhausted string) logs the reason and returns null, and the caller takes the two-step path.
209
+ */
210
+ export function slimPassOrNull(text, kind, label, swap) {
211
+ const startedAt = Date.now();
212
+ try {
213
+ const slim = slimPass(text, kind, swap);
214
+ logSlimmed(kind, label.sessionId, slim.stats, startedAt);
215
+ return slim;
216
+ }
217
+ catch (error) {
218
+ logSlimFailed(kind, error, text.length);
219
+ return null;
220
+ }
144
221
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.263",
3
+ "version": "0.2.265",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {