@wrongstack/core 0.256.1 → 0.257.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.
@@ -6,16 +6,6 @@ import * as path2 from 'path';
6
6
  import * as os from 'os';
7
7
  import { EventEmitter } from 'events';
8
8
 
9
- // src/utils/expect-defined.ts
10
- function expectDefined(value, label) {
11
- if (value === null || value === void 0) {
12
- const err = new Error("Expected value to be defined");
13
- err.name = "ExpectDefinedError";
14
- throw err;
15
- }
16
- return value;
17
- }
18
-
19
9
  // src/utils/token-estimate.ts
20
10
  var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
21
11
  var CALIBRATION_GLOBAL_KEY = "__global__";
@@ -35,12 +25,15 @@ function getCachedEstimate(key, compute) {
35
25
  const existing = ESTIMATE_CACHE.get(key);
36
26
  if (existing !== void 0) return existing;
37
27
  if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
38
- const keys = [...ESTIMATE_CACHE.keys()];
39
- for (let i = 0; i < Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4); i++) {
40
- ESTIMATE_CACHE.delete(expectDefined(keys[i]));
28
+ let evicted = 0;
29
+ const maxEvict = Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4);
30
+ for (const k of ESTIMATE_CACHE.keys()) {
31
+ if (evicted >= maxEvict) break;
32
+ ESTIMATE_CACHE.delete(k);
33
+ evicted++;
41
34
  }
42
35
  }
43
- const estimate = compute();
36
+ const estimate = compute(key);
44
37
  ESTIMATE_CACHE.set(key, estimate);
45
38
  return estimate;
46
39
  }
@@ -49,13 +42,11 @@ function estimateToolInputTokens(input) {
49
42
  if (input === null || typeof input !== "object") {
50
43
  return RoughTokenEstimate(String(input));
51
44
  }
52
- const key = JSON.stringify(input);
53
- return getCachedEstimate(key, () => RoughTokenEstimate(key));
45
+ return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
54
46
  }
55
47
  function estimateToolResultTokens(content) {
56
48
  if (typeof content === "string") return RoughTokenEstimate(content);
57
- const key = JSON.stringify(content);
58
- return getCachedEstimate(key, () => RoughTokenEstimate(key));
49
+ return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
59
50
  }
60
51
  function estimateTextTokens(text) {
61
52
  return RoughTokenEstimate(text);
@@ -162,6 +153,16 @@ function estimateRequestTokensCalibrated(messages, systemPrompt, tools, calibrat
162
153
  return result;
163
154
  }
164
155
 
156
+ // src/utils/expect-defined.ts
157
+ function expectDefined(value, label) {
158
+ if (value === null || value === void 0) {
159
+ const err = new Error("Expected value to be defined");
160
+ err.name = "ExpectDefinedError";
161
+ throw err;
162
+ }
163
+ return value;
164
+ }
165
+
165
166
  // src/utils/message-invariants.ts
166
167
  function repairToolUseAdjacency(messages) {
167
168
  const removedToolUses = [];
@@ -260,6 +261,25 @@ function isTextBlock(b) {
260
261
  }
261
262
 
262
263
  // src/execution/compaction-core.ts
264
+ function emitCompactionMetrics(event, metrics) {
265
+ console.log(
266
+ JSON.stringify({
267
+ level: "debug",
268
+ event,
269
+ messageCount: metrics.messageCount,
270
+ preserveStart: metrics.preserveStart,
271
+ fastPathIterations: metrics.fastPathIterations,
272
+ fastPathInnerIterations: metrics.fastPathInnerIterations,
273
+ // Ratios — anything > 2.0 indicates the inner loop is running more than expected
274
+ fastPathInnerPerOuter: metrics.fastPathIterations > 0 ? metrics.fastPathInnerIterations / metrics.fastPathIterations : 0,
275
+ fullPassIterations: metrics.fullPassIterations,
276
+ fullPassInnerIterations: metrics.fullPassInnerIterations,
277
+ fullPassInnerPerOuter: metrics.fullPassIterations > 0 ? metrics.fullPassInnerIterations / metrics.fullPassIterations : 0,
278
+ tokensSaved: metrics.tokensSaved,
279
+ changed: metrics.changed
280
+ })
281
+ );
282
+ }
263
283
  var estimateMessages = estimateMessageTokens;
264
284
  function hasTextContent(m) {
265
285
  if (typeof m.content === "string") return m.content.trim().length > 0;
@@ -276,37 +296,78 @@ function findPreserveStart(messages, preserveK) {
276
296
  preserveStart = i;
277
297
  }
278
298
  }
299
+ let forwardWalkIterations = 0;
300
+ let forwardWalkInnerIterations = 0;
279
301
  for (let i = preserveStart; i < messages.length; i++) {
302
+ forwardWalkIterations++;
280
303
  const m = messages[i];
281
304
  if (!m || typeof m.content === "string" || !Array.isArray(m.content)) continue;
282
- const hasToolUse3 = m.content.some((b) => b.type === "tool_use");
305
+ const hasToolUse3 = m.content.some((b) => {
306
+ forwardWalkInnerIterations++;
307
+ return b.type === "tool_use";
308
+ });
283
309
  if (hasToolUse3 && i + 1 < messages.length) {
284
310
  const next = messages[i + 1];
285
- if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => b.type === "tool_result")) {
311
+ if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => {
312
+ forwardWalkInnerIterations++;
313
+ return b.type === "tool_result";
314
+ })) {
286
315
  preserveStart = i + 1;
287
316
  }
288
317
  }
289
318
  }
319
+ console.log(
320
+ JSON.stringify({
321
+ level: "debug",
322
+ event: "compaction.find_preserve_start.ended",
323
+ messageCount: messages.length,
324
+ preserveK,
325
+ preserveStart,
326
+ forwardWalkIterations,
327
+ forwardWalkInnerIterations,
328
+ forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
329
+ })
330
+ );
290
331
  return preserveStart;
291
332
  }
292
333
  function eliseOldToolResults(messages, opts) {
293
334
  const preserveStart = findPreserveStart(messages, opts.preserveK);
294
335
  let hasOversized = false;
336
+ let fastPathIterations = 0;
337
+ let fastPathInnerIterations = 0;
295
338
  for (let i = 0; i < preserveStart && !hasOversized; i++) {
339
+ fastPathIterations++;
296
340
  const msg = messages[i];
297
341
  if (!msg || !Array.isArray(msg.content)) continue;
298
342
  for (const b of msg.content) {
343
+ fastPathInnerIterations++;
299
344
  if (b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold) {
300
345
  hasOversized = true;
301
346
  break;
302
347
  }
303
348
  }
304
349
  }
350
+ emitCompactionMetrics(
351
+ hasOversized ? "compaction.elision.fast_path.oversized_found" : "compaction.elision.fast_path.no_oversized",
352
+ {
353
+ messageCount: messages.length,
354
+ preserveStart,
355
+ fastPathIterations,
356
+ fastPathInnerIterations,
357
+ fullPassIterations: 0,
358
+ fullPassInnerIterations: 0,
359
+ tokensSaved: 0,
360
+ changed: false
361
+ }
362
+ );
305
363
  if (!hasOversized) return { messages, saved: 0, changed: false };
306
364
  let saved = 0;
307
365
  let changed = false;
366
+ let fullPassIterations = 0;
367
+ let fullPassInnerIterations = 0;
308
368
  const next = new Array(messages.length);
309
369
  for (let i = 0; i < messages.length; i++) {
370
+ fullPassIterations++;
310
371
  const msg = messages[i];
311
372
  if (i >= preserveStart || !msg || !Array.isArray(msg.content)) {
312
373
  next[i] = msg;
@@ -332,7 +393,33 @@ function eliseOldToolResults(messages, opts) {
332
393
  next[i] = { ...msg, content: newContent };
333
394
  changed = true;
334
395
  }
396
+ fullPassInnerIterations += original.length;
397
+ if (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1") {
398
+ const ratio = fullPassInnerIterations / fullPassIterations;
399
+ if (ratio > 10) {
400
+ console.error(
401
+ JSON.stringify({
402
+ level: "error",
403
+ event: "compaction.elision.regression",
404
+ message: `fullPassInnerPerOuter=${ratio.toFixed(2)} exceeds threshold 10 \u2014 possible O(n\xB7m) regression`,
405
+ messageCount: messages.length,
406
+ fullPassIterations,
407
+ fullPassInnerIterations
408
+ })
409
+ );
410
+ }
411
+ }
335
412
  }
413
+ emitCompactionMetrics("compaction.elision.full_pass.ended", {
414
+ messageCount: messages.length,
415
+ preserveStart,
416
+ fastPathIterations,
417
+ fastPathInnerIterations,
418
+ fullPassIterations,
419
+ fullPassInnerIterations,
420
+ tokensSaved: saved,
421
+ changed
422
+ });
336
423
  return { messages: changed ? next : messages, saved, changed };
337
424
  }
338
425
  function buildLosslessDigest(messages) {