acp-kernel 0.0.89 → 0.0.90-pr.420.271

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/dist/index.js CHANGED
@@ -111,33 +111,45 @@ function summaryMessageId(blockId) {
111
111
  function isSummaryMessageId(id) {
112
112
  return id.startsWith(SUMMARY_ID_PREFIX);
113
113
  }
114
+ function baseIdOf(id) {
115
+ const hash = id.indexOf("#");
116
+ return hash > 0 ? id.substring(0, hash) : id;
117
+ }
118
+ function isCovered(id, coveredBases) {
119
+ return coveredBases.has(baseIdOf(id));
120
+ }
114
121
  function isRenderedSummaryMessage(message) {
115
122
  return isSummaryMessageId(message.id) && message.role === "system" && message.contentType === "text";
116
123
  }
117
124
  function prune(messages, state, options = {}) {
118
125
  const covered = coveredMessageIds(state);
119
126
  if (covered.size === 0) return [...messages];
127
+ const coveredBases = /* @__PURE__ */ new Set();
128
+ for (const id of covered) coveredBases.add(baseIdOf(id));
120
129
  const inject = options.injectSummaries ?? true;
121
130
  const firstUserIndex = messages.findIndex(
122
131
  (message) => message.role === "user"
123
132
  );
124
- const indexById = /* @__PURE__ */ new Map();
133
+ const baseIndexById = /* @__PURE__ */ new Map();
125
134
  const summaryIndexById = /* @__PURE__ */ new Map();
126
135
  messages.forEach((message, index) => {
127
- indexById.set(message.id, index);
136
+ const base = baseIdOf(message.id);
137
+ const existing = baseIndexById.get(base);
138
+ if (existing === void 0 || index < existing)
139
+ baseIndexById.set(base, index);
128
140
  if (isRenderedSummaryMessage(message))
129
141
  summaryIndexById.set(message.id, index);
130
142
  });
131
- const anchors = inject ? collectSummaryAnchors(state, indexById, summaryIndexById) : [];
143
+ const anchors = inject ? collectSummaryAnchors(state, baseIndexById, summaryIndexById) : [];
132
144
  return stripOrphanedReasoning(
133
145
  stripOrphanedToolResults(
134
146
  stripOrphanedToolCalls(
135
- rebuildMessages(messages, covered, firstUserIndex, anchors)
147
+ rebuildMessages(messages, coveredBases, firstUserIndex, anchors)
136
148
  )
137
149
  )
138
150
  );
139
151
  }
140
- function collectSummaryAnchors(state, indexById, summaryIndexById) {
152
+ function collectSummaryAnchors(state, baseIndexById, summaryIndexById) {
141
153
  const anchors = [];
142
154
  for (const block of activeBlocks(state)) {
143
155
  const existingIndex = summaryIndexById.get(summaryMessageId(block.blockId));
@@ -152,7 +164,7 @@ function collectSummaryAnchors(state, indexById, summaryIndexById) {
152
164
  }
153
165
  let earliest = null;
154
166
  for (const id of block.effectiveMessageIds) {
155
- const index = indexById.get(id);
167
+ const index = baseIndexById.get(baseIdOf(id));
156
168
  if (index !== void 0 && (earliest === null || index < earliest)) {
157
169
  earliest = index;
158
170
  }
@@ -197,7 +209,7 @@ function pairSafeAnchorIndex(messages, index) {
197
209
  }
198
210
  return safe;
199
211
  }
200
- function rebuildMessages(messages, covered, firstUserIndex, anchors) {
212
+ function rebuildMessages(messages, coveredBases, firstUserIndex, anchors) {
201
213
  const safeAnchors = anchors.map((anchor) => ({
202
214
  ...anchor,
203
215
  insertAt: pairSafeAnchorIndex(messages, anchor.insertAt)
@@ -215,7 +227,7 @@ function rebuildMessages(messages, covered, firstUserIndex, anchors) {
215
227
  result.push(messages[index]);
216
228
  continue;
217
229
  }
218
- if (covered.has(messages[index].id)) continue;
230
+ if (isCovered(messages[index].id, coveredBases)) continue;
219
231
  if (isRenderedSummaryMessage(messages[index]) && anchoredSummaryIds.has(messages[index].id))
220
232
  continue;
221
233
  result.push(messages[index]);
@@ -281,6 +293,8 @@ function stripOrphanedReasoning(messages) {
281
293
  // src/sync.ts
282
294
  function syncBlocks(messages, state) {
283
295
  const presentIds = new Set(messages.map((message) => message.id));
296
+ const presentBases = /* @__PURE__ */ new Set();
297
+ for (const message of messages) presentBases.add(baseIdOf(message.id));
284
298
  const deactivated = [];
285
299
  const result = {
286
300
  blocks: state.blocks.map((block) => ({
@@ -330,7 +344,7 @@ function syncBlocks(messages, state) {
330
344
  continue;
331
345
  }
332
346
  block.active = true;
333
- const stillPresent = block.effectiveMessageIds.some((id) => presentIds.has(id)) || presentIds.has(summaryMessageId(block.blockId));
347
+ const stillPresent = block.effectiveMessageIds.some((id) => presentBases.has(baseIdOf(id))) || presentIds.has(summaryMessageId(block.blockId));
334
348
  if (!stillPresent) {
335
349
  block.active = false;
336
350
  deactivated.push(block.blockId);
@@ -1248,7 +1262,12 @@ function hasMediaPayload(msg) {
1248
1262
  if (m.rawOpenaiContent != null) return true;
1249
1263
  if (Array.isArray(m.rawOpenaiContentParts) && m.rawOpenaiContentParts.length > 0)
1250
1264
  return true;
1251
- if (isObjWith(m.rawAnthropicBlock, "type", "image")) return true;
1265
+ const ab = m.rawAnthropicBlock;
1266
+ if (isObjWith(ab, "type", "image")) return true;
1267
+ if (isObjWith(ab, "type", "tool_result")) {
1268
+ const content = ab.content;
1269
+ if (Array.isArray(content)) return content.some((p) => !isObjWith(p, "type", "text"));
1270
+ }
1252
1271
  const item = m.rawResponsesItem;
1253
1272
  if (isObjWith(item, "type", "input_image")) return true;
1254
1273
  if (item && typeof item === "object") {
@@ -1612,8 +1631,10 @@ function resolveBoundaries(input) {
1612
1631
  if (endAnchor.snapped) snappedBoundaries.push(endAnchor.snapped);
1613
1632
  let startIndex = startAnchor.index;
1614
1633
  let endIndex = endAnchor.index;
1634
+ let reversedNote;
1615
1635
  if (startIndex > endIndex) {
1616
1636
  [startIndex, endIndex] = [endIndex, startIndex];
1637
+ reversedNote = `note: refs were given reversed (${start.raw}\u2192${end.raw}), normalized to ${end.raw}..${start.raw}`;
1617
1638
  }
1618
1639
  const messageIds = [];
1619
1640
  for (let index = startIndex; index <= endIndex; index++) {
@@ -1640,7 +1661,8 @@ function resolveBoundaries(input) {
1640
1661
  nestedBlockIds,
1641
1662
  boundaryKind,
1642
1663
  protectedGaps,
1643
- snappedBoundaries
1664
+ snappedBoundaries,
1665
+ reversedNote
1644
1666
  };
1645
1667
  }
1646
1668
  function resolveAnchorIndex(boundary, state, indexByMessageId, endpoint) {
@@ -1665,6 +1687,27 @@ function resolveAnchorIndex(boundary, state, indexByMessageId, endpoint) {
1665
1687
  snapped: `${label}="${boundary.raw}" refers to a message already compressed into an active block \u2014 anchored to the active block covering it instead.`
1666
1688
  };
1667
1689
  }
1690
+ const paddedRef = formatPaddedRef(boundary.numericId);
1691
+ if (state.hiddenOrphanRefs?.includes(paddedRef)) {
1692
+ const neighbor = snapToNeighborVisible(
1693
+ state,
1694
+ indexByMessageId,
1695
+ boundary.numericId,
1696
+ endpoint
1697
+ );
1698
+ if (neighbor !== null) {
1699
+ const dir = endpoint === "start" ? "the next visible message after it" : "the previous visible message before it";
1700
+ return {
1701
+ index: neighbor,
1702
+ snapped: `${label}="${boundary.raw}" is a hidden orphan compress call (no matching block) \u2014 snapped to ${dir} instead.`
1703
+ };
1704
+ }
1705
+ throw new BoundaryNotFoundError(
1706
+ "consumed",
1707
+ endpoint,
1708
+ `${label}="${boundary.raw}" is a hidden orphan compress call with no adjacent visible message to anchor to \u2014 run acp_status for current refs.`
1709
+ );
1710
+ }
1668
1711
  throw new BoundaryNotFoundError(
1669
1712
  "consumed",
1670
1713
  endpoint,
@@ -1741,6 +1784,26 @@ function inheritedContentIds(state, block) {
1741
1784
  }
1742
1785
  return ids;
1743
1786
  }
1787
+ function snapToNeighborVisible(state, indexByMessageId, refNumber, endpoint) {
1788
+ let bestIndex = null;
1789
+ let bestRef = null;
1790
+ for (const [rawId, index] of indexByMessageId) {
1791
+ const refText = state.messageRefs.byRaw[rawId];
1792
+ if (!refText || refText === BLOCKED_REF) continue;
1793
+ const ref = refToIndex(refText);
1794
+ if (ref === null) continue;
1795
+ if (endpoint === "start") {
1796
+ if (ref > refNumber && (bestRef === null || ref < bestRef)) {
1797
+ bestRef = ref;
1798
+ bestIndex = index;
1799
+ }
1800
+ } else if (ref < refNumber && (bestRef === null || ref > bestRef)) {
1801
+ bestRef = ref;
1802
+ bestIndex = index;
1803
+ }
1804
+ }
1805
+ return bestIndex;
1806
+ }
1744
1807
  function formatPaddedRef(index) {
1745
1808
  return `m${String(index).padStart(5, "0")}`;
1746
1809
  }
@@ -1771,6 +1834,214 @@ function earliestIndexOfIds(ids, indexByMessageId) {
1771
1834
  return earliest;
1772
1835
  }
1773
1836
 
1837
+ // src/block-map.ts
1838
+ function refNum(ref) {
1839
+ const m = ref.match(/\d+/);
1840
+ return m ? parseInt(m[0], 10) : 0;
1841
+ }
1842
+ var M_REF = /^m\d+$/;
1843
+ function resolveBlockSpan(block, byRaw) {
1844
+ if (block.startRef && block.endRef && M_REF.test(block.startRef) && M_REF.test(block.endRef)) {
1845
+ return { startRef: block.startRef, endRef: block.endRef };
1846
+ }
1847
+ const refs = block.effectiveMessageIds.map((id) => byRaw[id]).filter((r) => typeof r === "string" && r !== "BLOCKED");
1848
+ if (refs.length === 0) return null;
1849
+ const sorted = [...refs].sort((a, b) => refNum(a) - refNum(b));
1850
+ return { startRef: sorted[0], endRef: sorted[sorted.length - 1] };
1851
+ }
1852
+ function activeBlockSpans(state) {
1853
+ const spans = [];
1854
+ for (const block of state.blocks) {
1855
+ if (!block.active) continue;
1856
+ const span = resolveBlockSpan(block, state.messageRefs.byRaw);
1857
+ if (!span) continue;
1858
+ spans.push({ blockId: block.blockId, tier: block.tier, ...span });
1859
+ }
1860
+ return spans;
1861
+ }
1862
+ function formatCreatedBlocks(state, newBlocks) {
1863
+ const parts = [];
1864
+ for (const block of newBlocks) {
1865
+ const span = resolveBlockSpan(block, state.messageRefs.byRaw);
1866
+ parts.push(
1867
+ span ? `${block.blockId}=${span.startRef}\u2013${span.endRef}` : block.blockId
1868
+ );
1869
+ }
1870
+ return parts.length > 0 ? `blocks: ${parts.join(", ")}` : "";
1871
+ }
1872
+
1873
+ // src/decompress.ts
1874
+ function parseBlockIdArg(arg) {
1875
+ const normalized = arg.trim().toLowerCase();
1876
+ const refMatch = /^b0*(\d+)$/.exec(normalized);
1877
+ if (refMatch && refMatch[1] !== void 0) return `b${refMatch[1]}`;
1878
+ const numMatch = /^(\d+)$/.exec(normalized);
1879
+ if (numMatch && numMatch[1] !== void 0) return `b${numMatch[1]}`;
1880
+ return null;
1881
+ }
1882
+ function findBlocksOverlappingMessages(state, messageIds) {
1883
+ if (messageIds.size === 0) return [];
1884
+ const matched = [];
1885
+ for (const block of state.blocks) {
1886
+ if (!block.active) continue;
1887
+ if (block.effectiveMessageIds.some((id) => messageIds.has(id))) {
1888
+ matched.push(block);
1889
+ }
1890
+ }
1891
+ return matched.sort((a, b) => numericPart(a.blockId) - numericPart(b.blockId));
1892
+ }
1893
+ function findActiveAncestor(state, blockId) {
1894
+ const start = state.blocks.find((b) => b.blockId === blockId);
1895
+ if (!start) return null;
1896
+ const queue = [...start.directBlockIds];
1897
+ const visited = /* @__PURE__ */ new Set();
1898
+ while (queue.length > 0) {
1899
+ const currentId = queue.shift();
1900
+ if (visited.has(currentId)) continue;
1901
+ visited.add(currentId);
1902
+ const current = state.blocks.find((b) => b.blockId === currentId);
1903
+ if (!current) continue;
1904
+ if (current.active) return current.blockId;
1905
+ for (const ancestorId of current.directBlockIds) {
1906
+ if (!visited.has(ancestorId)) queue.push(ancestorId);
1907
+ }
1908
+ }
1909
+ return null;
1910
+ }
1911
+ function deactivateBlock(state, blockIds, options = {}) {
1912
+ const targets = new Set(blockIds);
1913
+ const updated = state.blocks.map((block) => {
1914
+ if (!targets.has(block.blockId) || !block.active) return block;
1915
+ return {
1916
+ ...block,
1917
+ active: false,
1918
+ durationMs: block.durationMs,
1919
+ createdAt: block.createdAt
1920
+ };
1921
+ });
1922
+ let final = updated;
1923
+ if (options.deep) {
1924
+ const visited = /* @__PURE__ */ new Set();
1925
+ const queue = [];
1926
+ for (const id of blockIds) {
1927
+ const block = updated.find((b) => b.blockId === id);
1928
+ if (block) queue.push(...block.directBlockIds);
1929
+ }
1930
+ while (queue.length > 0) {
1931
+ const id = queue.shift();
1932
+ if (visited.has(id)) continue;
1933
+ visited.add(id);
1934
+ final = final.map((block) => {
1935
+ if (block.blockId !== id) return block;
1936
+ queue.push(...block.directBlockIds);
1937
+ return block.active ? { ...block, active: false } : block;
1938
+ });
1939
+ }
1940
+ }
1941
+ return { ...state, blocks: final };
1942
+ }
1943
+ function buildRestoredContentPreview(messages, beforeActiveMessageIds, state) {
1944
+ const restored = [];
1945
+ for (const message of messages) {
1946
+ if (!beforeActiveMessageIds.has(message.id)) continue;
1947
+ const stillCovered = state.blocks.some(
1948
+ (b) => b.active && b.effectiveMessageIds.includes(message.id)
1949
+ );
1950
+ if (!stillCovered) restored.push(message);
1951
+ }
1952
+ if (restored.length === 0) return { preview: "", restoredCount: 0 };
1953
+ const lines = [];
1954
+ let totalLength = 0;
1955
+ const MAX_PREVIEW = 2e3;
1956
+ const MAX_PER_MESSAGE = 200;
1957
+ for (const message of restored) {
1958
+ if (totalLength >= MAX_PREVIEW) break;
1959
+ const text = message.text ?? "";
1960
+ const truncated = text.length > MAX_PER_MESSAGE ? clampPrefix(text, MAX_PER_MESSAGE) + "..." : text;
1961
+ const label = message.toolName && message.contentType !== "text" ? `${message.toolName}: ${truncated}` : `[${message.role}] ${truncated}`;
1962
+ lines.push(label);
1963
+ totalLength += label.length + 1;
1964
+ }
1965
+ return { preview: lines.join("\n"), restoredCount: restored.length };
1966
+ }
1967
+ function collectBlockContent(state, block, messages, options = {}) {
1968
+ const full = options.full ?? false;
1969
+ const targetIds = new Set(block.effectiveMessageIds);
1970
+ if (full) {
1971
+ const msgs = messages.filter((m) => targetIds.has(m.id));
1972
+ if (msgs.length === 0) return { text: "", count: 0 };
1973
+ return { text: msgs.map(formatMessage).join("\n\n"), count: msgs.length };
1974
+ }
1975
+ const nestedChildren = [];
1976
+ const nestedCovered = /* @__PURE__ */ new Set();
1977
+ for (const childId of block.directBlockIds) {
1978
+ const child = state.blocks.find((b) => b.blockId === childId);
1979
+ if (!child?.active) continue;
1980
+ nestedChildren.push(child);
1981
+ for (const id of child.effectiveMessageIds) nestedCovered.add(id);
1982
+ }
1983
+ const parts = [];
1984
+ for (const child of nestedChildren) {
1985
+ const label = child.topic ? `${child.blockId}: ${child.topic}` : child.blockId;
1986
+ parts.push(`${SUMMARY_HEADER} \u2014 ${label}
1987
+ ${child.summary}`);
1988
+ }
1989
+ let directCount = 0;
1990
+ for (const m of messages) {
1991
+ if (targetIds.has(m.id) && !nestedCovered.has(m.id)) {
1992
+ parts.push(formatMessage(m));
1993
+ directCount++;
1994
+ }
1995
+ }
1996
+ const count = directCount + nestedChildren.length;
1997
+ if (count === 0) return { text: "", count: 0 };
1998
+ return { text: parts.join("\n\n"), count };
1999
+ }
2000
+ function formatMessage(message) {
2001
+ const text = message.text ?? "";
2002
+ if (message.toolName && message.contentType !== "text") {
2003
+ return `[${message.role} \u2022 ${message.toolName}]
2004
+ ${text}`;
2005
+ }
2006
+ return `[${message.role}]
2007
+ ${text}`;
2008
+ }
2009
+ function numericPart(blockId) {
2010
+ const match = /^b(\d+)$/.exec(blockId);
2011
+ return match && match[1] !== void 0 ? Number(match[1]) : 0;
2012
+ }
2013
+ function markBlockRestoredInline(state, blockId) {
2014
+ const existing = state.blocks.find((b) => b.blockId === blockId);
2015
+ if (!existing) return { state, result: null };
2016
+ const blocks = state.blocks.map(
2017
+ (block) => block.blockId === blockId ? { ...block, restoredInline: true } : block
2018
+ );
2019
+ const span = resolveBlockSpan(existing, state.messageRefs.byRaw);
2020
+ const result = {
2021
+ restored: true,
2022
+ blockId,
2023
+ ...span ? { restoredStartRef: span.startRef, restoredEndRef: span.endRef } : {}
2024
+ };
2025
+ return { state: { ...state, blocks }, result };
2026
+ }
2027
+ function activeAncestorIds(state, blockId) {
2028
+ const out = [];
2029
+ const visited = /* @__PURE__ */ new Set([blockId]);
2030
+ let frontier = [blockId];
2031
+ while (frontier.length > 0) {
2032
+ const next = [];
2033
+ for (const block of state.blocks) {
2034
+ if (visited.has(block.blockId)) continue;
2035
+ if (!block.directBlockIds.some((id) => frontier.includes(id))) continue;
2036
+ visited.add(block.blockId);
2037
+ if (block.active) out.push(block.blockId);
2038
+ next.push(block.blockId);
2039
+ }
2040
+ frontier = next;
2041
+ }
2042
+ return out;
2043
+ }
2044
+
1774
2045
  // src/truncate-tools.ts
1775
2046
  var TRUNCATION_MARKER = "[truncated for context space]";
1776
2047
  var DEFAULTS = {
@@ -1955,14 +2226,26 @@ function hideConsumedCompressCalls(state, messages) {
1955
2226
  }
1956
2227
  }
1957
2228
  let hidden = 0;
2229
+ const hiddenOrphanRefs = [];
2230
+ const rememberHiddenRef = (message) => {
2231
+ if (message.toolCallId && allBlockCallIds.has(message.toolCallId)) {
2232
+ return;
2233
+ }
2234
+ const ref = state.messageRefs.byRaw[message.id];
2235
+ if (ref && ref !== BLOCKED_REF && !hiddenOrphanRefs.includes(ref)) {
2236
+ hiddenOrphanRefs.push(ref);
2237
+ }
2238
+ };
1958
2239
  const result = [];
1959
2240
  for (const message of messages) {
1960
2241
  if (message.toolName === "compress" && message.contentType === "tool-call" && (!message.toolCallId || !keepCallIds.has(message.toolCallId))) {
1961
2242
  hidden++;
2243
+ rememberHiddenRef(message);
1962
2244
  continue;
1963
2245
  }
1964
2246
  if (message.contentType === "tool-result" && message.toolCallId && hiddenCallIds.has(message.toolCallId)) {
1965
2247
  hidden++;
2248
+ rememberHiddenRef(message);
1966
2249
  continue;
1967
2250
  }
1968
2251
  if (message.toolName === "compress" && message.contentType === "tool-call" && message.toolCallId && keepCallIds.has(message.toolCallId)) {
@@ -1982,7 +2265,7 @@ function hideConsumedCompressCalls(state, messages) {
1982
2265
  }
1983
2266
  result.push(message);
1984
2267
  }
1985
- return { messages: result, hidden };
2268
+ return { messages: result, hidden, hiddenOrphanRefs };
1986
2269
  }
1987
2270
 
1988
2271
  // src/absorb.ts
@@ -3632,42 +3915,6 @@ function applyMessageFilters(messages, config) {
3632
3915
  return { messages: working, ...tally };
3633
3916
  }
3634
3917
 
3635
- // src/block-map.ts
3636
- function refNum(ref) {
3637
- const m = ref.match(/\d+/);
3638
- return m ? parseInt(m[0], 10) : 0;
3639
- }
3640
- var M_REF = /^m\d+$/;
3641
- function resolveBlockSpan(block, byRaw) {
3642
- if (block.startRef && block.endRef && M_REF.test(block.startRef) && M_REF.test(block.endRef)) {
3643
- return { startRef: block.startRef, endRef: block.endRef };
3644
- }
3645
- const refs = block.effectiveMessageIds.map((id) => byRaw[id]).filter((r) => typeof r === "string" && r !== "BLOCKED");
3646
- if (refs.length === 0) return null;
3647
- const sorted = [...refs].sort((a, b) => refNum(a) - refNum(b));
3648
- return { startRef: sorted[0], endRef: sorted[sorted.length - 1] };
3649
- }
3650
- function activeBlockSpans(state) {
3651
- const spans = [];
3652
- for (const block of state.blocks) {
3653
- if (!block.active) continue;
3654
- const span = resolveBlockSpan(block, state.messageRefs.byRaw);
3655
- if (!span) continue;
3656
- spans.push({ blockId: block.blockId, tier: block.tier, ...span });
3657
- }
3658
- return spans;
3659
- }
3660
- function formatCreatedBlocks(state, newBlocks) {
3661
- const parts = [];
3662
- for (const block of newBlocks) {
3663
- const span = resolveBlockSpan(block, state.messageRefs.byRaw);
3664
- parts.push(
3665
- span ? `${block.blockId}=${span.startRef}\u2013${span.endRef}` : block.blockId
3666
- );
3667
- }
3668
- return parts.length > 0 ? `blocks: ${parts.join(", ")}` : "";
3669
- }
3670
-
3671
3918
  // src/render-refs.ts
3672
3919
  function formatTokens(tokens) {
3673
3920
  if (tokens < 1e3) return String(tokens);
@@ -3740,6 +3987,11 @@ function createRenderRefsNode(strategy) {
3740
3987
  }
3741
3988
  var renderRefsNode = createRenderRefsNode("all");
3742
3989
 
3990
+ // src/message-kind.ts
3991
+ function isToolMessage(message) {
3992
+ return message.contentType === "tool-call" || message.contentType === "tool-result";
3993
+ }
3994
+
3743
3995
  // src/tool-pairs.ts
3744
3996
  function adjustBoundariesForToolPairs(startIndex, endIndex, messages, maxScan = 20) {
3745
3997
  const callIdsInRange = /* @__PURE__ */ new Set();
@@ -3944,9 +4196,6 @@ function computeIntegrityWithdrawals(messages, foldedIds) {
3944
4196
  function estimateTextTokens(text) {
3945
4197
  return Math.ceil(text.length / 4);
3946
4198
  }
3947
- function isToolMessage(message) {
3948
- return message.contentType === "tool-call" || message.contentType === "tool-result";
3949
- }
3950
4199
  function isSyntheticOrPruned(message, state) {
3951
4200
  if (message.text?.startsWith("[Compressed conversation section]")) return true;
3952
4201
  for (const block of state.blocks) {
@@ -4266,6 +4515,79 @@ function tierActionHint(config, state) {
4266
4515
  }
4267
4516
  return "";
4268
4517
  }
4518
+ function requestedRefoldSpan(state, spec) {
4519
+ const nums = [];
4520
+ for (const ref of [spec.startRef, spec.endRef]) {
4521
+ const parsed = parseBoundary(ref);
4522
+ if (!parsed) return null;
4523
+ if (parsed.kind === "message") {
4524
+ nums.push(parsed.numericId);
4525
+ continue;
4526
+ }
4527
+ const block = blockById(state, `b${parsed.numericId}`);
4528
+ if (!block) return null;
4529
+ const span = resolveBlockSpan(block, state.messageRefs.byRaw);
4530
+ if (!span) return null;
4531
+ const lo = parseBoundary(span.startRef);
4532
+ const hi = parseBoundary(span.endRef);
4533
+ if (!lo || !hi) return null;
4534
+ nums.push(lo.numericId, hi.numericId);
4535
+ }
4536
+ return { lo: Math.min(...nums), hi: Math.max(...nums) };
4537
+ }
4538
+ function evaluateRefold(state, spec) {
4539
+ const span = requestedRefoldSpan(state, spec);
4540
+ if (!span) return { kind: "blocked", reasons: [] };
4541
+ const reasons = [];
4542
+ const blocks = [];
4543
+ for (const block of state.blocks) {
4544
+ if (!block.active) continue;
4545
+ const resolved = resolveBlockSpan(block, state.messageRefs.byRaw);
4546
+ if (!resolved) {
4547
+ reasons.push(`partially covered ${block.blockId}`);
4548
+ continue;
4549
+ }
4550
+ const ownLo = parseBoundary(resolved.startRef)?.numericId;
4551
+ const ownHi = parseBoundary(resolved.endRef)?.numericId;
4552
+ if (ownLo === void 0 || ownHi === void 0) continue;
4553
+ if (ownHi < span.lo || ownLo > span.hi) continue;
4554
+ if (!block.restoredInline) {
4555
+ reasons.push(`blocked by ${block.blockId} (not restored)`);
4556
+ continue;
4557
+ }
4558
+ if (ownLo < span.lo || ownHi > span.hi) {
4559
+ reasons.push(`partially covered ${block.blockId}`);
4560
+ continue;
4561
+ }
4562
+ const blockers = activeAncestorIds(state, block.blockId).filter(
4563
+ (ancestorId) => !blockById(state, ancestorId)?.restoredInline
4564
+ );
4565
+ if (blockers.length > 0) {
4566
+ for (const id of blockers)
4567
+ reasons.push(`blocked by ${id} (not restored)`);
4568
+ continue;
4569
+ }
4570
+ blocks.push(block);
4571
+ }
4572
+ const uniqueReasons = [...new Set(reasons)];
4573
+ if (uniqueReasons.length > 0)
4574
+ return { kind: "blocked", reasons: uniqueReasons };
4575
+ if (blocks.length > 0) return { kind: "refold", blocks };
4576
+ return { kind: "blocked", reasons: [] };
4577
+ }
4578
+ function applyRefolds(input) {
4579
+ validateSummaryLength(input.spec, input.config.compress);
4580
+ const targets = new Set(input.blockIds);
4581
+ input.state.blocks = input.state.blocks.map(
4582
+ (block) => targets.has(block.blockId) ? {
4583
+ ...block,
4584
+ summary: input.spec.summary,
4585
+ topic: input.spec.topic ?? block.topic,
4586
+ runId: input.runId,
4587
+ restoredInline: false
4588
+ } : block
4589
+ );
4590
+ }
4269
4591
  function createCore(ports = {}) {
4270
4592
  const countTokens = ports.countTokens ?? defaultCountTokens;
4271
4593
  function applyCompression(input) {
@@ -4275,6 +4597,7 @@ function createCore(ports = {}) {
4275
4597
  let tokensCompressed = 0;
4276
4598
  const errors = [];
4277
4599
  const warnings = [];
4600
+ const notes = [];
4278
4601
  const protectedMessageIds = input.protectedMessageIds ?? computeProtectedRefs(
4279
4602
  input.messages,
4280
4603
  input.state,
@@ -4325,6 +4648,18 @@ function createCore(ports = {}) {
4325
4648
  if (resolution.status === "ok") resolvableCount++;
4326
4649
  else if (resolution.status === "unknown") unknownCount++;
4327
4650
  }
4651
+ const refoldDecisions = /* @__PURE__ */ new Map();
4652
+ const isRefoldCandidate = (resolution) => resolution.status === "consumed" || resolution.status === "ok" && resolution.resolved.boundaryKind !== "block" && resolution.resolved.messageIds.every(
4653
+ (id) => preExistingCoverage.has(id)
4654
+ );
4655
+ for (const [spec, resolution] of classifications) {
4656
+ if (isRefoldCandidate(resolution)) {
4657
+ refoldDecisions.set(spec, evaluateRefold(state, spec));
4658
+ }
4659
+ }
4660
+ const allRefold = input.ranges.length > 0 && unknownCount === 0 && [...classifications.entries()].every(
4661
+ ([spec, resolution]) => isRefoldCandidate(resolution) && refoldDecisions.get(spec)?.kind === "refold"
4662
+ );
4328
4663
  const rangeSpans = [];
4329
4664
  for (const [spec, resolution] of classifications) {
4330
4665
  if (resolution.status !== "ok") continue;
@@ -4363,7 +4698,7 @@ function createCore(ports = {}) {
4363
4698
  totalRangeChars += msg?.text?.length ?? 0;
4364
4699
  }
4365
4700
  }
4366
- if (!hasBlockBoundaryRange && totalRangeChars < input.config.compress.minCompressRange) {
4701
+ if (!allRefold && !hasBlockBoundaryRange && totalRangeChars < input.config.compress.minCompressRange) {
4367
4702
  const diagnostics = refGateDiagnostics(
4368
4703
  state,
4369
4704
  input.ranges.length,
@@ -4372,10 +4707,50 @@ function createCore(ports = {}) {
4372
4707
  const firstConsumed = consumedRanges[0];
4373
4708
  const covering = firstConsumed ? coveringBlockIds(state, firstConsumed) : [];
4374
4709
  const coverDetail = covering.length > 0 ? `its content is already summarized in active block(s) ${covering.join(", ")}${covering.length === 1 ? ` \u2014 use search_context or decompress ${covering[0]} if you need details from it` : ""}` : `its refs no longer point to directly compressible content (stale block ref(s) distilled or consumed by higher-tier blocks)`;
4710
+ const refoldReasons = [
4711
+ ...new Set(
4712
+ consumedRanges.flatMap((spec) => {
4713
+ const decision = refoldDecisions.get(spec);
4714
+ return decision?.kind === "blocked" ? decision.reasons : [];
4715
+ })
4716
+ )
4717
+ ];
4718
+ const refoldSuffix = (reasons) => reasons.length > 0 ? ` Refold blocked: ${reasons.join("; ")}. Restore the affected block(s) inline (decompress with inline:true), then recompressing the same range updates them in place` : "";
4719
+ const refoldDetail = refoldSuffix(refoldReasons);
4720
+ const okBlockedReasons = [
4721
+ ...new Set(
4722
+ [...classifications.entries()].flatMap(([spec, resolution]) => {
4723
+ if (skipSpecs.has(spec) || resolution.status !== "ok") return [];
4724
+ const decision = refoldDecisions.get(spec);
4725
+ return decision?.kind === "blocked" ? decision.reasons : [];
4726
+ })
4727
+ )
4728
+ ];
4375
4729
  const danglingRefs = consumedRanges.flatMap(
4376
4730
  (spec) => danglingMessageRefs(state, input.messages, spec)
4377
4731
  );
4378
- const gateMessage = resolvableCount === 0 && consumedRanges.length === 0 && unknownCount > 0 ? `None of the ${input.ranges.length} requested range(s) resolved \u2014 every ref is unknown to this session. Refs are per-session snapshots, assigned once when a message is first rendered; no compress reassigns them, so unknown refs cannot come from an earlier compress in this session. They come from a different generation: a previous session instance (switching model or upstream mid-conversation starts a fresh session whose refs restart at m00001), the generation before a native-compaction rebase (which also resets refs to m00001), or a typo. ${diagnostics} Run acp_status, then call the compress tool again using only the refs it reports.` : consumedRanges.length > 0 ? danglingRefs.length > 0 ? `Requested range(s) cannot be anchored (e.g. ${firstConsumed.startRef}..${firstConsumed.endRef}) \u2014 the refs exist in this session's ref map, but the messages they point to are no longer in the visible context and no active block covers them: the message content changed (or the message was filtered out of the view) and now carries a new ref, leaving your old refs dangling. ${diagnostics} Run acp_status, then call the compress tool again using only the refs it reports.` : `Requested range(s) already compressed (e.g. ${firstConsumed.startRef}..${firstConsumed.endRef}) \u2014 ${coverDetail}. Nothing new to compress in that window. ${diagnostics} Continue the task, or run acp_status and target one of the CURRENT compressible ranges it reports.${tierActionHint(input.config, state)}` : `Total compressible content too small (${totalRangeChars} chars across ${countedRanges} range(s), min ${input.config.compress.minCompressRange}). Combine more messages into your range(s) to meet the threshold.`;
4732
+ let gateMessage = resolvableCount === 0 && consumedRanges.length === 0 && unknownCount > 0 ? `None of the ${input.ranges.length} requested range(s) resolved \u2014 every ref is unknown to this session. Refs are per-session snapshots, assigned once when a message is first rendered; no compress reassigns them, so unknown refs cannot come from an earlier compress in this session. They come from a different generation: a previous session instance (switching model or upstream mid-conversation starts a fresh session whose refs restart at m00001), the generation before a native-compaction rebase (which also resets refs to m00001), or a typo. ${diagnostics} Run acp_status, then call the compress tool again using only the refs it reports.` : consumedRanges.length > 0 ? danglingRefs.length > 0 ? `Requested range(s) cannot be anchored (e.g. ${firstConsumed.startRef}..${firstConsumed.endRef}) \u2014 the refs exist in this session's ref map, but the messages they point to are no longer in the visible context and no active block covers them: the message content changed (or the message was filtered out of the view) and now carries a new ref, leaving your old refs dangling. ${diagnostics} Run acp_status, then call the compress tool again using only the refs it reports.` : `Requested range(s) already compressed (e.g. ${firstConsumed.startRef}..${firstConsumed.endRef}) \u2014 ${coverDetail}${refoldDetail}. Nothing new to compress in that window. ${diagnostics} Continue the task, or run acp_status and target one of the CURRENT compressible ranges it reports.${tierActionHint(input.config, state)}` : countedRanges > 0 ? `Total compressible content too small (${totalRangeChars} chars across ${countedRanges} range(s), min ${input.config.compress.minCompressRange}). Combine more messages into your range(s) to meet the threshold.${refoldSuffix(okBlockedReasons)}` : null;
4733
+ if (gateMessage === null) {
4734
+ return {
4735
+ state: input.state,
4736
+ result: {
4737
+ blocksCreated: 0,
4738
+ tokensCompressed: 0,
4739
+ errors: [...classificationErrors],
4740
+ warnings: []
4741
+ }
4742
+ };
4743
+ }
4744
+ const reversalNotes = [];
4745
+ for (const [spec, resolution] of classifications) {
4746
+ if (resolution.status === "ok" && !skipSpecs.has(spec)) {
4747
+ const note = resolution.resolved.reversedNote;
4748
+ if (note) reversalNotes.push(note);
4749
+ }
4750
+ }
4751
+ if (reversalNotes.length > 0) {
4752
+ gateMessage += ` ${reversalNotes.join(" ")}`;
4753
+ }
4379
4754
  return {
4380
4755
  state: input.state,
4381
4756
  result: {
@@ -4392,8 +4767,30 @@ function createCore(ports = {}) {
4392
4767
  const resolution = classifications.get(spec);
4393
4768
  if (resolution === void 0) continue;
4394
4769
  if (resolution.status === "consumed") {
4770
+ const decision = refoldDecisions.get(spec);
4771
+ if (decision?.kind === "refold") {
4772
+ try {
4773
+ applyRefolds({
4774
+ spec,
4775
+ state,
4776
+ runId,
4777
+ config: input.config,
4778
+ blockIds: decision.blocks.map((block) => block.blockId)
4779
+ });
4780
+ blocksCreated += decision.blocks.length;
4781
+ } catch (error) {
4782
+ errors.push(
4783
+ rangeError(
4784
+ spec,
4785
+ error instanceof Error ? error.message : String(error)
4786
+ )
4787
+ );
4788
+ }
4789
+ continue;
4790
+ }
4791
+ const reasons = decision?.kind === "blocked" ? decision.reasons : [];
4395
4792
  warnings.push(
4396
- `Skipped range (${spec.startRef}..${spec.endRef}) \u2014 already compressed (messages consumed by existing block(s)); nothing to compress.`
4793
+ `Skipped range (${spec.startRef}..${spec.endRef}) \u2014 already compressed${reasons.length > 0 ? `: ${reasons.join("; ")}` : " (messages consumed by existing block(s))"}; nothing to compress.`
4397
4794
  );
4398
4795
  continue;
4399
4796
  }
@@ -4402,6 +4799,8 @@ function createCore(ports = {}) {
4402
4799
  continue;
4403
4800
  }
4404
4801
  warnings.push(...resolution.resolved.snappedBoundaries);
4802
+ const note = resolution.resolved.reversedNote;
4803
+ if (note) notes.push(note);
4405
4804
  try {
4406
4805
  const outcome = applySingleRange({
4407
4806
  spec,
@@ -4413,7 +4812,7 @@ function createCore(ports = {}) {
4413
4812
  countTokens,
4414
4813
  preExistingCoverage
4415
4814
  });
4416
- blocksCreated++;
4815
+ blocksCreated += outcome.refolded ? outcome.refolded.length : 1;
4417
4816
  tokensCompressed += outcome.tokens;
4418
4817
  warnings.push(...outcome.warnings);
4419
4818
  } catch (error) {
@@ -4435,7 +4834,13 @@ function createCore(ports = {}) {
4435
4834
  }
4436
4835
  return {
4437
4836
  state,
4438
- result: { blocksCreated, tokensCompressed, errors, warnings }
4837
+ result: {
4838
+ blocksCreated,
4839
+ tokensCompressed,
4840
+ errors,
4841
+ warnings,
4842
+ ...notes.length > 0 ? { notes } : {}
4843
+ }
4439
4844
  };
4440
4845
  }
4441
4846
  function processTurn(input) {
@@ -4619,7 +5024,11 @@ var hideCompressCallsNode = {
4619
5024
  name: "hide-compress-calls",
4620
5025
  run(io) {
4621
5026
  const hidden = hideConsumedCompressCalls(io.state, io.messages);
4622
- return { ...io, messages: hidden.messages };
5027
+ return {
5028
+ ...io,
5029
+ messages: hidden.messages,
5030
+ state: { ...io.state, hiddenOrphanRefs: hidden.hiddenOrphanRefs }
5031
+ };
4623
5032
  }
4624
5033
  };
4625
5034
  var recommendNode = {
@@ -4877,6 +5286,21 @@ function applySingleRange(input) {
4877
5286
  }
4878
5287
  }
4879
5288
  if (!isBlockBoundary && filteredIds.length === 0 && consumedBlockIds.length > 0) {
5289
+ const decision = evaluateRefold(input.state, input.spec);
5290
+ if (decision.kind === "refold") {
5291
+ applyRefolds({
5292
+ spec: input.spec,
5293
+ state: input.state,
5294
+ runId: input.runId,
5295
+ config: input.config,
5296
+ blockIds: decision.blocks.map((block2) => block2.blockId)
5297
+ });
5298
+ return {
5299
+ tokens: 0,
5300
+ warnings,
5301
+ refolded: decision.blocks.map((block2) => block2.blockId)
5302
+ };
5303
+ }
4880
5304
  const first = consumedBlockIds[0];
4881
5305
  const last = consumedBlockIds[consumedBlockIds.length - 1];
4882
5306
  throw new Error(
@@ -4957,7 +5381,15 @@ function applyPairBoundaryAdjustments(resolved, messages) {
4957
5381
  }
4958
5382
  function validateCompressionRange(input, directMessageIds, consumedBlockCount) {
4959
5383
  const cfg = input.config.compress;
4960
- const summary = input.spec.summary?.trim() ?? "";
5384
+ validateSummaryLength(input.spec, cfg);
5385
+ if (directMessageIds.length === 0 && consumedBlockCount === 0) {
5386
+ throw new Error(
5387
+ "Range contains no compressible messages \u2014 all are already covered by active blocks or protected."
5388
+ );
5389
+ }
5390
+ }
5391
+ function validateSummaryLength(spec, cfg) {
5392
+ const summary = spec.summary?.trim() ?? "";
4961
5393
  if (summary.length === 0) {
4962
5394
  throw new Error(
4963
5395
  "Summary is empty \u2014 provide a meaningful summary of the compressed range."
@@ -4968,17 +5400,12 @@ function validateCompressionRange(input, directMessageIds, consumedBlockCount) {
4968
5400
  `Summary too short (${summary.length} chars, min ${cfg.minSummaryLength}). The summary must capture the compressed range's key information.`
4969
5401
  );
4970
5402
  }
4971
- const effectiveMax = input.spec.summaryMaxChars ?? cfg.maxSummaryLength;
5403
+ const effectiveMax = spec.summaryMaxChars ?? cfg.maxSummaryLength;
4972
5404
  if (effectiveMax > 0 && summary.length > effectiveMax) {
4973
5405
  throw new Error(
4974
5406
  `Summary too long (${summary.length} chars, max ${effectiveMax}). Strip noise \u2014 keep critical paths, decisions, errors, and code references. Or pass summaryMaxChars to increase the limit \u2014 don't lose critical info just to fit.`
4975
5407
  );
4976
5408
  }
4977
- if (directMessageIds.length === 0 && consumedBlockCount === 0) {
4978
- throw new Error(
4979
- "Range contains no compressible messages \u2014 all are already covered by active blocks or protected."
4980
- );
4981
- }
4982
5409
  }
4983
5410
  function filterProtectedToolMessages(directMessageIds, messages, config) {
4984
5411
  const protectedCallIds = /* @__PURE__ */ new Set();
@@ -5219,7 +5646,7 @@ function computeContextBreakdown(messages, total, growth, countTokens) {
5219
5646
  const tokens = countMessageTokens(msg, count);
5220
5647
  if (msg.text?.startsWith("[Compressed conversation section]")) {
5221
5648
  summaries += tokens;
5222
- } else if (msg.contentType === "tool-call" || msg.contentType === "tool-result") {
5649
+ } else if (isToolMessage(msg)) {
5223
5650
  tool += tokens;
5224
5651
  } else if (msg.role === "system") {
5225
5652
  system += tokens;
@@ -5251,7 +5678,8 @@ function cloneState(state) {
5251
5678
  nextRuleId: state.nextRuleId,
5252
5679
  terminalStreak: state.terminalStreak,
5253
5680
  nextBlockId: state.nextBlockId,
5254
- nextRunId: state.nextRunId
5681
+ nextRunId: state.nextRunId,
5682
+ hiddenOrphanRefs: state.hiddenOrphanRefs ? [...state.hiddenOrphanRefs] : void 0
5255
5683
  };
5256
5684
  }
5257
5685
  function scoreRelevance(block, terms) {
@@ -5521,147 +5949,6 @@ function defaultPackSources(opts) {
5521
5949
  return sources;
5522
5950
  }
5523
5951
 
5524
- // src/decompress.ts
5525
- function parseBlockIdArg(arg) {
5526
- const normalized = arg.trim().toLowerCase();
5527
- const refMatch = /^b0*(\d+)$/.exec(normalized);
5528
- if (refMatch && refMatch[1] !== void 0) return `b${refMatch[1]}`;
5529
- const numMatch = /^(\d+)$/.exec(normalized);
5530
- if (numMatch && numMatch[1] !== void 0) return `b${numMatch[1]}`;
5531
- return null;
5532
- }
5533
- function findBlocksOverlappingMessages(state, messageIds) {
5534
- if (messageIds.size === 0) return [];
5535
- const matched = [];
5536
- for (const block of state.blocks) {
5537
- if (!block.active) continue;
5538
- if (block.effectiveMessageIds.some((id) => messageIds.has(id))) {
5539
- matched.push(block);
5540
- }
5541
- }
5542
- return matched.sort((a, b) => numericPart(a.blockId) - numericPart(b.blockId));
5543
- }
5544
- function findActiveAncestor(state, blockId) {
5545
- const start = state.blocks.find((b) => b.blockId === blockId);
5546
- if (!start) return null;
5547
- const queue = [...start.directBlockIds];
5548
- const visited = /* @__PURE__ */ new Set();
5549
- while (queue.length > 0) {
5550
- const currentId = queue.shift();
5551
- if (visited.has(currentId)) continue;
5552
- visited.add(currentId);
5553
- const current = state.blocks.find((b) => b.blockId === currentId);
5554
- if (!current) continue;
5555
- if (current.active) return current.blockId;
5556
- for (const ancestorId of current.directBlockIds) {
5557
- if (!visited.has(ancestorId)) queue.push(ancestorId);
5558
- }
5559
- }
5560
- return null;
5561
- }
5562
- function deactivateBlock(state, blockIds, options = {}) {
5563
- const targets = new Set(blockIds);
5564
- const updated = state.blocks.map((block) => {
5565
- if (!targets.has(block.blockId) || !block.active) return block;
5566
- return {
5567
- ...block,
5568
- active: false,
5569
- durationMs: block.durationMs,
5570
- createdAt: block.createdAt
5571
- };
5572
- });
5573
- let final = updated;
5574
- if (options.deep) {
5575
- const visited = /* @__PURE__ */ new Set();
5576
- const queue = [];
5577
- for (const id of blockIds) {
5578
- const block = updated.find((b) => b.blockId === id);
5579
- if (block) queue.push(...block.directBlockIds);
5580
- }
5581
- while (queue.length > 0) {
5582
- const id = queue.shift();
5583
- if (visited.has(id)) continue;
5584
- visited.add(id);
5585
- final = final.map((block) => {
5586
- if (block.blockId !== id) return block;
5587
- queue.push(...block.directBlockIds);
5588
- return block.active ? { ...block, active: false } : block;
5589
- });
5590
- }
5591
- }
5592
- return { ...state, blocks: final };
5593
- }
5594
- function buildRestoredContentPreview(messages, beforeActiveMessageIds, state) {
5595
- const restored = [];
5596
- for (const message of messages) {
5597
- if (!beforeActiveMessageIds.has(message.id)) continue;
5598
- const stillCovered = state.blocks.some(
5599
- (b) => b.active && b.effectiveMessageIds.includes(message.id)
5600
- );
5601
- if (!stillCovered) restored.push(message);
5602
- }
5603
- if (restored.length === 0) return { preview: "", restoredCount: 0 };
5604
- const lines = [];
5605
- let totalLength = 0;
5606
- const MAX_PREVIEW = 2e3;
5607
- const MAX_PER_MESSAGE = 200;
5608
- for (const message of restored) {
5609
- if (totalLength >= MAX_PREVIEW) break;
5610
- const text = message.text ?? "";
5611
- const truncated = text.length > MAX_PER_MESSAGE ? clampPrefix(text, MAX_PER_MESSAGE) + "..." : text;
5612
- const label = message.toolName && message.contentType !== "text" ? `${message.toolName}: ${truncated}` : `[${message.role}] ${truncated}`;
5613
- lines.push(label);
5614
- totalLength += label.length + 1;
5615
- }
5616
- return { preview: lines.join("\n"), restoredCount: restored.length };
5617
- }
5618
- function collectBlockContent(state, block, messages, options = {}) {
5619
- const full = options.full ?? false;
5620
- const targetIds = new Set(block.effectiveMessageIds);
5621
- if (full) {
5622
- const msgs = messages.filter((m) => targetIds.has(m.id));
5623
- if (msgs.length === 0) return { text: "", count: 0 };
5624
- return { text: msgs.map(formatMessage).join("\n\n"), count: msgs.length };
5625
- }
5626
- const nestedChildren = [];
5627
- const nestedCovered = /* @__PURE__ */ new Set();
5628
- for (const childId of block.directBlockIds) {
5629
- const child = state.blocks.find((b) => b.blockId === childId);
5630
- if (!child?.active) continue;
5631
- nestedChildren.push(child);
5632
- for (const id of child.effectiveMessageIds) nestedCovered.add(id);
5633
- }
5634
- const parts = [];
5635
- for (const child of nestedChildren) {
5636
- const label = child.topic ? `${child.blockId}: ${child.topic}` : child.blockId;
5637
- parts.push(`${SUMMARY_HEADER} \u2014 ${label}
5638
- ${child.summary}`);
5639
- }
5640
- let directCount = 0;
5641
- for (const m of messages) {
5642
- if (targetIds.has(m.id) && !nestedCovered.has(m.id)) {
5643
- parts.push(formatMessage(m));
5644
- directCount++;
5645
- }
5646
- }
5647
- const count = directCount + nestedChildren.length;
5648
- if (count === 0) return { text: "", count: 0 };
5649
- return { text: parts.join("\n\n"), count };
5650
- }
5651
- function formatMessage(message) {
5652
- const text = message.text ?? "";
5653
- if (message.toolName && message.contentType !== "text") {
5654
- return `[${message.role} \u2022 ${message.toolName}]
5655
- ${text}`;
5656
- }
5657
- return `[${message.role}]
5658
- ${text}`;
5659
- }
5660
- function numericPart(blockId) {
5661
- const match = /^b(\d+)$/.exec(blockId);
5662
- return match && match[1] !== void 0 ? Number(match[1]) : 0;
5663
- }
5664
-
5665
5952
  // src/report.ts
5666
5953
  function formatTokens2(n) {
5667
5954
  if (!Number.isFinite(n) || n <= 0) return "0";
@@ -5686,14 +5973,16 @@ function tierLabel(block) {
5686
5973
  }
5687
5974
  function tierBreakdown(blocks, countTokens) {
5688
5975
  const tierTokens = {};
5976
+ const tierCounts = {};
5689
5977
  for (const block of blocks) {
5690
5978
  tierTokens[block.tier] = (tierTokens[block.tier] ?? 0) + summaryTokensOf(block, countTokens);
5979
+ tierCounts[block.tier] = (tierCounts[block.tier] ?? 0) + 1;
5691
5980
  }
5692
5981
  const tiers = Object.keys(tierTokens).map(Number);
5693
5982
  if (tiers.length <= 1) return null;
5694
5983
  const parts = [];
5695
5984
  for (const tier of [1, 2, 3]) {
5696
- if (tierTokens[tier]) parts.push(`T${tier}: ${formatTokens2(tierTokens[tier])}`);
5985
+ if (tierTokens[tier]) parts.push(`T${tier}: ${formatTokens2(tierTokens[tier])} (${tierCounts[tier]} blocks)`);
5697
5986
  }
5698
5987
  return parts.join(" | ");
5699
5988
  }
@@ -5719,8 +6008,9 @@ function collectVisible(messages, state, countTokens) {
5719
6008
  const ref = refForRaw(state.messageRefs, message.id);
5720
6009
  if (!ref) return;
5721
6010
  const tokens = countMessageTokens(message, countTokens);
5722
- const tool = isToolMessage(message) ? message.toolName ?? (message.toolCallId ? toolCallNames.get(message.toolCallId) : void 0) ?? "tool" : "text";
5723
- if (tokens > 0) visible.push({ ref, tokens, tool, index });
6011
+ const isTool = isToolMessage(message);
6012
+ const tool = isTool ? message.toolName ?? (message.toolCallId ? toolCallNames.get(message.toolCallId) : void 0) ?? "tool" : "text";
6013
+ if (tokens > 0) visible.push({ ref, tokens, tool, isTool, index });
5724
6014
  });
5725
6015
  return { visible, summaryTokens };
5726
6016
  }
@@ -5763,8 +6053,8 @@ function renderOverview(visible, summaryTokens, blocks, state, countTokens, limi
5763
6053
  toolTypeMap.set(message.tool, (toolTypeMap.get(message.tool) ?? 0) + message.tokens);
5764
6054
  }
5765
6055
  const topTool = [...toolTypeMap.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
5766
- const totalTool = visible.filter((m) => m.tool !== "text").reduce((sum, m) => sum + m.tokens, 0);
5767
- const totalText = visible.filter((m) => m.tool === "text").reduce((sum, m) => sum + m.tokens, 0);
6056
+ const totalTool = visible.filter((m) => m.isTool).reduce((sum, m) => sum + m.tokens, 0);
6057
+ const totalText = visible.filter((m) => !m.isTool).reduce((sum, m) => sum + m.tokens, 0);
5768
6058
  const total = summaryTokens + totalTool + totalText;
5769
6059
  lines.push("CONTEXT BREAKDOWN");
5770
6060
  lines.push(
@@ -5800,6 +6090,11 @@ function renderOverview(visible, summaryTokens, blocks, state, countTokens, limi
5800
6090
  ` ${block.blockId} (${tierLabel(block)}) ${formatTokens2(eff)}\u2192${formatTokens2(summaryTokensOf(block, countTokens))} ${block.effectiveMessageIds.length} msgs "${topic}"`
5801
6091
  );
5802
6092
  }
6093
+ if (blocks.length > limit) {
6094
+ lines.push(
6095
+ ` ... and ${blocks.length - limit} more blocks not shown (scope:"compressed", limit:${blocks.length} for full list)`
6096
+ );
6097
+ }
5803
6098
  }
5804
6099
  lines.push("");
5805
6100
  lines.push(
@@ -7482,7 +7777,11 @@ function build(text) {
7482
7777
  }
7483
7778
  function docFeatures(text) {
7484
7779
  const hit = cache.get(text);
7485
- if (hit) return hit;
7780
+ if (hit) {
7781
+ cache.delete(text);
7782
+ cache.set(text, hit);
7783
+ return hit;
7784
+ }
7486
7785
  const f = build(text);
7487
7786
  if (text.length > 0 && text.length <= capChars) {
7488
7787
  while (cachedChars + text.length > capChars && cache.size > 0) {
@@ -7593,8 +7892,8 @@ var hybridAlgorithm = {
7593
7892
  score(docs, query) {
7594
7893
  const bm = bm25Algorithm.score(docs, query);
7595
7894
  const fz = fuzzyAlgorithm.score(docs, query);
7596
- const maxBm = Math.max(...bm.map((r) => r.score), 1e-9);
7597
- const maxFz = Math.max(...fz.map((r) => r.score), 1e-9);
7895
+ const maxBm = bm.reduce((m, r) => r.score > m ? r.score : m, 1e-9);
7896
+ const maxFz = fz.reduce((m, r) => r.score > m ? r.score : m, 1e-9);
7598
7897
  const bmMap = new Map(bm.map((r) => [r.ref, r.score / maxBm]));
7599
7898
  const fzMap = new Map(fz.map((r) => [r.ref, r.score / maxFz]));
7600
7899
  return docs.map((d) => ({
@@ -7941,6 +8240,7 @@ export {
7941
8240
  TIER3_CONDENSE_RULES,
7942
8241
  VERBOSITY_LEVELS,
7943
8242
  VIABLE_RANGE_MIN_TOKENS,
8243
+ activeAncestorIds,
7944
8244
  activeBlockSpans,
7945
8245
  activeBlocks,
7946
8246
  addRule,
@@ -7958,6 +8258,7 @@ export {
7958
8258
  applySectionOverrides,
7959
8259
  applySteeringToPrompt,
7960
8260
  assignRefs,
8261
+ baseIdOf,
7961
8262
  blockById,
7962
8263
  blockDocs,
7963
8264
  blockVisibleInRange,
@@ -8034,6 +8335,7 @@ export {
8034
8335
  imageShrinksForRef,
8035
8336
  indexToRef,
8036
8337
  isAbsorbCandidate,
8338
+ isCovered,
8037
8339
  isImageFullRestored,
8038
8340
  isMessageLatestProtected,
8039
8341
  isMessageProtected,
@@ -8041,6 +8343,7 @@ export {
8041
8343
  isRetrievedMessage,
8042
8344
  isStoredPlaceholderText,
8043
8345
  isSummaryMessageId,
8346
+ isToolMessage,
8044
8347
  isValidPackName,
8045
8348
  jsonFoldPlugin,
8046
8349
  leanPack,
@@ -8050,6 +8353,7 @@ export {
8050
8353
  listSearchAlgorithms,
8051
8354
  logSelectPlugin,
8052
8355
  makeIO,
8356
+ markBlockRestoredInline,
8053
8357
  matchSession,
8054
8358
  matchToolPattern,
8055
8359
  messageDocs,