context-mode 1.0.87 → 1.0.89

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/build/truncate.js CHANGED
@@ -6,6 +6,43 @@
6
6
  * consumer can import them without pulling in the full store or executor.
7
7
  */
8
8
  // ─────────────────────────────────────────────────────────
9
+ // Internal: byte-safe prefix
10
+ // ─────────────────────────────────────────────────────────
11
+ /**
12
+ * Return the longest character-prefix of `str` whose UTF-8 encoding is at
13
+ * most `maxBytes` bytes. Uses binary search to avoid O(n²) scanning. Returns
14
+ * "" when `maxBytes` is <= 0 so callers never exceed their budget.
15
+ *
16
+ * Guards against splitting a UTF-16 surrogate pair: if the prefix would end
17
+ * on a lone high surrogate, back off one code unit so the result round-trips
18
+ * through UTF-8 without producing a U+FFFD replacement character.
19
+ */
20
+ function byteSafePrefix(str, maxBytes) {
21
+ if (maxBytes <= 0)
22
+ return "";
23
+ if (Buffer.byteLength(str) <= maxBytes)
24
+ return str;
25
+ let lo = 0;
26
+ let hi = str.length;
27
+ while (lo < hi) {
28
+ const mid = (lo + hi + 1) >> 1;
29
+ if (Buffer.byteLength(str.slice(0, mid)) <= maxBytes) {
30
+ lo = mid;
31
+ }
32
+ else {
33
+ hi = mid - 1;
34
+ }
35
+ }
36
+ // If we landed between a high and low surrogate, back off so the prefix
37
+ // ends on a valid code point boundary.
38
+ if (lo > 0) {
39
+ const code = str.charCodeAt(lo - 1);
40
+ if (code >= 0xd800 && code <= 0xdbff)
41
+ lo -= 1;
42
+ }
43
+ return str.slice(0, lo);
44
+ }
45
+ // ─────────────────────────────────────────────────────────
9
46
  // JSON truncation
10
47
  // ─────────────────────────────────────────────────────────
11
48
  /**
@@ -14,6 +51,9 @@
14
51
  * "... [truncated]" is appended. The result is NOT guaranteed to be valid
15
52
  * JSON after truncation — it is suitable only for display/logging.
16
53
  *
54
+ * The returned string is always <= `maxBytes` bytes. When `maxBytes` is
55
+ * smaller than the marker, the marker itself is byte-safely truncated.
56
+ *
17
57
  * @param value - Any JSON-serializable value.
18
58
  * @param maxBytes - Maximum byte length of the returned string.
19
59
  * @param indent - JSON indentation spaces (default 2). Pass 0 for compact.
@@ -22,24 +62,13 @@ export function truncateJSON(value, maxBytes, indent = 2) {
22
62
  const serialized = JSON.stringify(value, null, indent) ?? "null";
23
63
  if (Buffer.byteLength(serialized) <= maxBytes)
24
64
  return serialized;
25
- // Find the largest character slice that stays within maxBytes once encoded.
26
- // Buffer.byteLength is O(n) but we only call it once per truncation.
27
65
  const marker = "... [truncated]";
28
66
  const markerBytes = Buffer.byteLength(marker);
29
- const budget = maxBytes - markerBytes;
30
- // Binary-search for the right character count avoids O(n²) scanning.
31
- let lo = 0;
32
- let hi = serialized.length;
33
- while (lo < hi) {
34
- const mid = (lo + hi + 1) >> 1;
35
- if (Buffer.byteLength(serialized.slice(0, mid)) <= budget) {
36
- lo = mid;
37
- }
38
- else {
39
- hi = mid - 1;
40
- }
41
- }
42
- return serialized.slice(0, lo) + marker;
67
+ // Degenerate budget: can't fit serialized content + marker. Fit as much of
68
+ // the marker as we can so the return still honors `maxBytes`.
69
+ if (maxBytes <= markerBytes)
70
+ return byteSafePrefix(marker, maxBytes);
71
+ return byteSafePrefix(serialized, maxBytes - markerBytes) + marker;
43
72
  }
44
73
  // ─────────────────────────────────────────────────────────
45
74
  // XML / HTML escaping
@@ -68,6 +97,9 @@ export function escapeXML(str) {
68
97
  * byte-safe slice with an ellipsis appended. Useful for single-value fields
69
98
  * (e.g., tool response strings) where head+tail splitting is not needed.
70
99
  *
100
+ * The returned string is always <= `maxBytes` bytes. When `maxBytes` is
101
+ * smaller than the ellipsis marker, the marker itself is byte-safely truncated.
102
+ *
71
103
  * @param str - Input string.
72
104
  * @param maxBytes - Hard byte cap.
73
105
  */
@@ -76,17 +108,7 @@ export function capBytes(str, maxBytes) {
76
108
  return str;
77
109
  const marker = "...";
78
110
  const markerBytes = Buffer.byteLength(marker);
79
- const budget = maxBytes - markerBytes;
80
- let lo = 0;
81
- let hi = str.length;
82
- while (lo < hi) {
83
- const mid = (lo + hi + 1) >> 1;
84
- if (Buffer.byteLength(str.slice(0, mid)) <= budget) {
85
- lo = mid;
86
- }
87
- else {
88
- hi = mid - 1;
89
- }
90
- }
91
- return str.slice(0, lo) + marker;
111
+ if (maxBytes <= markerBytes)
112
+ return byteSafePrefix(marker, maxBytes);
113
+ return byteSafePrefix(str, maxBytes - markerBytes) + marker;
92
114
  }