greprag 5.74.18 → 5.74.19

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.
@@ -59,6 +59,7 @@ exports.classifyContent = classifyContent;
59
59
  exports.crushField = crushField;
60
60
  exports.crushMessages = crushMessages;
61
61
  const crush_1 = require("./crush");
62
+ const crypto_1 = require("crypto");
62
63
  // ---------- CCR marker (inlined — see header: ccr-store.ts drags sql.js) ------
63
64
  /** First 16 hex of sha256 — must match ccr-store.ts CCR_MARKER_HASH_LEN. */
64
65
  const CCR_MARKER_HASH_LEN = 16;
@@ -157,13 +158,19 @@ function runEngine(type, content) {
157
158
  * passthrough, and token-validate-or-revert. The stash side effect fires ONLY
158
159
  * on a real crush (after validation passes), so a reverted/skipped field is
159
160
  * never persisted. */
160
- function crushField(content, stash, thresholdBytes = exports.DEFAULT_THRESHOLD_BYTES) {
161
+ function crushField(content, stash, thresholdBytes = exports.DEFAULT_THRESHOLD_BYTES, seenHashes) {
161
162
  if (typeof content !== 'string')
162
163
  return null;
163
164
  if (CCR_MARKER_RE.test(content))
164
165
  return null; // idempotent
165
166
  if (Buffer.byteLength(content, 'utf8') < thresholdBytes)
166
167
  return null; // below floor
168
+ // Compute the stash hash first — if we've already stashed this exact
169
+ // content (e.g. it was retrieved via greprag retrieve), skip re-crushing
170
+ // to break the infinite marker→retrieve→same-marker loop.
171
+ const contentHash = (0, crypto_1.createHash)('sha256').update(content, 'utf8').digest('hex').slice(0, CCR_MARKER_HASH_LEN);
172
+ if (seenHashes?.has(contentHash))
173
+ return null;
167
174
  const type = classifyContent(content);
168
175
  const result = runEngine(type, content);
169
176
  if (result.stats.passthrough)
@@ -177,6 +184,7 @@ function crushField(content, stash, thresholdBytes = exports.DEFAULT_THRESHOLD_B
177
184
  if (finalTokens >= (0, crush_1.estimateTokens)(content))
178
185
  return null;
179
186
  const hash = stash(content, type);
187
+ seenHashes?.add(hash);
180
188
  return `${crushed}\n${ccrMarker(hash)}`;
181
189
  }
182
190
  // ---------- Message-array walker ----------------------------------------------
@@ -203,7 +211,7 @@ function resolveRetrieveCommand(p) {
203
211
  * ORIGINAL content, and crush is deterministic, so the reused value is exactly
204
212
  * what crushField would recompute; the original was already stashed on the
205
213
  * first (miss) crush, so skipping re-stash is safe for retrieve. */
206
- function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
214
+ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo, seenHashes) {
207
215
  const content = obj[key];
208
216
  if (typeof content !== 'string')
209
217
  return;
@@ -225,13 +233,13 @@ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
225
233
  stats.memoHits++;
226
234
  }
227
235
  else {
228
- replacement = crushField(content, stash, thresholdBytes);
236
+ replacement = crushField(content, stash, thresholdBytes, seenHashes);
229
237
  stats.crushFieldCalls++;
230
238
  memo.set(k, replacement);
231
239
  }
232
240
  }
233
241
  else {
234
- replacement = crushField(content, stash, thresholdBytes);
242
+ replacement = crushField(content, stash, thresholdBytes, seenHashes);
235
243
  stats.crushFieldCalls++;
236
244
  }
237
245
  if (replacement === null) {
@@ -247,30 +255,18 @@ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
247
255
  * - text part → `.text`
248
256
  * - tool part → `.state.output` (only when state.status === 'completed';
249
257
  * an 'error' state's `.error` is precious — left alone). */
250
- function processPart(part, stash, thresholdBytes, stats, memo) {
258
+ function processPart(part, stash, thresholdBytes, stats, memo, seenHashes) {
251
259
  if (!part || typeof part !== 'object')
252
260
  return;
253
261
  const p = part;
254
262
  if (p.type === 'text' && typeof p.text === 'string') {
255
- crushAndApply(p, 'text', stash, thresholdBytes, stats, memo);
263
+ crushAndApply(p, 'text', stash, thresholdBytes, stats, memo, seenHashes);
256
264
  return;
257
265
  }
258
266
  if (p.type === 'tool' && p.state && typeof p.state === 'object') {
259
267
  const state = p.state;
260
268
  if (state.status === 'completed' && typeof state.output === 'string') {
261
- // Skip re-crushing output of greprag retrieve the original was
262
- // already stashed once; crushing it again creates an infinite
263
- // marker→retrieve→same marker loop.
264
- // The tool name / command may live on p.input.command (tool_use
265
- // part) or on p.name / p.tool (tool_result part) depending on
266
- // the opencode message format version.
267
- const cmd = resolveRetrieveCommand(p);
268
- if (cmd && cmd.includes('greprag retrieve')) {
269
- stats.partsSkipped++;
270
- }
271
- else {
272
- crushAndApply(state, 'output', stash, thresholdBytes, stats, memo);
273
- }
269
+ crushAndApply(state, 'output', stash, thresholdBytes, stats, memo, seenHashes);
274
270
  }
275
271
  }
276
272
  }
@@ -285,6 +281,7 @@ function crushMessages(messages, opts) {
285
281
  crushFieldCalls: 0, memoHits: 0,
286
282
  };
287
283
  const thresholdBytes = opts.thresholdBytes ?? exports.DEFAULT_THRESHOLD_BYTES;
284
+ const seenHashes = opts.seenHashes ?? new Set();
288
285
  if (!Array.isArray(messages))
289
286
  return stats;
290
287
  for (const msg of messages) {
@@ -293,7 +290,7 @@ function crushMessages(messages, opts) {
293
290
  continue;
294
291
  for (const part of parts) {
295
292
  try {
296
- processPart(part, opts.stash, thresholdBytes, stats, opts.memo);
293
+ processPart(part, opts.stash, thresholdBytes, stats, opts.memo, seenHashes);
297
294
  }
298
295
  catch {
299
296
  // per-part swallow — a crush/stash failure must never break the turn
@@ -1600,6 +1600,7 @@ function crushCode(content, config = {}) {
1600
1600
  }
1601
1601
 
1602
1602
  // ../greprag/packages/cli/src/opencode-plugin-crush.ts
1603
+ var import_crypto = require("crypto");
1603
1604
  var CCR_MARKER_HASH_LEN = 16;
1604
1605
  function ccrMarker(hash) {
1605
1606
  return `<<ccr:${hash.slice(0, CCR_MARKER_HASH_LEN)}>>`;
@@ -1645,13 +1646,16 @@ function runEngine(type, content) {
1645
1646
  return crushLog(content);
1646
1647
  }
1647
1648
  }
1648
- function crushField(content, stash, thresholdBytes = DEFAULT_THRESHOLD_BYTES) {
1649
+ function crushField(content, stash, thresholdBytes = DEFAULT_THRESHOLD_BYTES, seenHashes) {
1649
1650
  if (typeof content !== "string")
1650
1651
  return null;
1651
1652
  if (CCR_MARKER_RE.test(content))
1652
1653
  return null;
1653
1654
  if (Buffer.byteLength(content, "utf8") < thresholdBytes)
1654
1655
  return null;
1656
+ const contentHash = (0, import_crypto.createHash)("sha256").update(content, "utf8").digest("hex").slice(0, CCR_MARKER_HASH_LEN);
1657
+ if (seenHashes?.has(contentHash))
1658
+ return null;
1655
1659
  const type = classifyContent(content);
1656
1660
  const result = runEngine(type, content);
1657
1661
  if (result.stats.passthrough)
@@ -1661,19 +1665,11 @@ function crushField(content, stash, thresholdBytes = DEFAULT_THRESHOLD_BYTES) {
1661
1665
  if (finalTokens >= estimateTokens(content))
1662
1666
  return null;
1663
1667
  const hash = stash(content, type);
1668
+ seenHashes?.add(hash);
1664
1669
  return `${crushed}
1665
1670
  ${ccrMarker(hash)}`;
1666
1671
  }
1667
- function resolveRetrieveCommand(p) {
1668
- const input = p.input;
1669
- if (input && typeof input.command === "string")
1670
- return input.command;
1671
- const toolName = typeof p.name === "string" && p.name || typeof p.tool === "string" && p.tool || typeof p.toolName === "string" && p.toolName;
1672
- if (toolName)
1673
- return toolName;
1674
- return "";
1675
- }
1676
- function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
1672
+ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo, seenHashes) {
1677
1673
  const content = obj[key];
1678
1674
  if (typeof content !== "string")
1679
1675
  return;
@@ -1694,12 +1690,12 @@ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
1694
1690
  replacement = memo.get(k) ?? null;
1695
1691
  stats.memoHits++;
1696
1692
  } else {
1697
- replacement = crushField(content, stash, thresholdBytes);
1693
+ replacement = crushField(content, stash, thresholdBytes, seenHashes);
1698
1694
  stats.crushFieldCalls++;
1699
1695
  memo.set(k, replacement);
1700
1696
  }
1701
1697
  } else {
1702
- replacement = crushField(content, stash, thresholdBytes);
1698
+ replacement = crushField(content, stash, thresholdBytes, seenHashes);
1703
1699
  stats.crushFieldCalls++;
1704
1700
  }
1705
1701
  if (replacement === null) {
@@ -1711,23 +1707,18 @@ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
1711
1707
  stats.partsCrushed++;
1712
1708
  stats.bytesAfter += Buffer.byteLength(replacement, "utf8");
1713
1709
  }
1714
- function processPart(part, stash, thresholdBytes, stats, memo) {
1710
+ function processPart(part, stash, thresholdBytes, stats, memo, seenHashes) {
1715
1711
  if (!part || typeof part !== "object")
1716
1712
  return;
1717
1713
  const p = part;
1718
1714
  if (p.type === "text" && typeof p.text === "string") {
1719
- crushAndApply(p, "text", stash, thresholdBytes, stats, memo);
1715
+ crushAndApply(p, "text", stash, thresholdBytes, stats, memo, seenHashes);
1720
1716
  return;
1721
1717
  }
1722
1718
  if (p.type === "tool" && p.state && typeof p.state === "object") {
1723
1719
  const state = p.state;
1724
1720
  if (state.status === "completed" && typeof state.output === "string") {
1725
- const cmd = resolveRetrieveCommand(p);
1726
- if (cmd && cmd.includes("greprag retrieve")) {
1727
- stats.partsSkipped++;
1728
- } else {
1729
- crushAndApply(state, "output", stash, thresholdBytes, stats, memo);
1730
- }
1721
+ crushAndApply(state, "output", stash, thresholdBytes, stats, memo, seenHashes);
1731
1722
  }
1732
1723
  }
1733
1724
  }
@@ -1742,6 +1733,7 @@ function crushMessages(messages, opts) {
1742
1733
  memoHits: 0
1743
1734
  };
1744
1735
  const thresholdBytes = opts.thresholdBytes ?? DEFAULT_THRESHOLD_BYTES;
1736
+ const seenHashes = opts.seenHashes ?? /* @__PURE__ */ new Set();
1745
1737
  if (!Array.isArray(messages))
1746
1738
  return stats;
1747
1739
  for (const msg of messages) {
@@ -1750,7 +1742,7 @@ function crushMessages(messages, opts) {
1750
1742
  continue;
1751
1743
  for (const part of parts) {
1752
1744
  try {
1753
- processPart(part, opts.stash, thresholdBytes, stats, opts.memo);
1745
+ processPart(part, opts.stash, thresholdBytes, stats, opts.memo, seenHashes);
1754
1746
  } catch {
1755
1747
  stats.partsSkipped++;
1756
1748
  }
@@ -4498,6 +4490,7 @@ function grepragBinCached() {
4498
4490
  return _cachedGrepragBin;
4499
4491
  }
4500
4492
  var _crushShapeLogged = false;
4493
+ var _crushSeenHashes;
4501
4494
  function inlineCrushEnabled() {
4502
4495
  const v = process.env.GREPRAG_OPENCODE_CRUSH;
4503
4496
  return v === "true" || v === "1";
@@ -4740,7 +4733,8 @@ ALWAYS pass \`--from-session ${sid82}\` when you run \`greprag send\`, so recipi
4740
4733
  const thresholdEnv = parseInt(process.env.GREPRAG_OPENCODE_CRUSH_MIN_BYTES || "", 10);
4741
4734
  const stats = crushMessages(msgs, {
4742
4735
  stash: makeCcrStash(),
4743
- thresholdBytes: Number.isFinite(thresholdEnv) && thresholdEnv > 0 ? thresholdEnv : void 0
4736
+ thresholdBytes: Number.isFinite(thresholdEnv) && thresholdEnv > 0 ? thresholdEnv : void 0,
4737
+ seenHashes: _crushSeenHashes ?? (_crushSeenHashes = /* @__PURE__ */ new Set())
4744
4738
  });
4745
4739
  if (stats.partsCrushed > 0 || stats.partsReverted > 0) {
4746
4740
  dlog(
@@ -792,6 +792,9 @@ function grepragBinCached() {
792
792
  * crusher's assumptions. See the chip report: live propagation + shape are
793
793
  * the two things only a real opencode restart can verify. */
794
794
  let _crushShapeLogged = false;
795
+ /** Per-session set of hashes already stashed — prevents re-crushing content
796
+ * that was already compressed once (e.g. retrieved via greprag retrieve). */
797
+ let _crushSeenHashes;
795
798
  /** GREPRAG_OPENCODE_CRUSH gate — opt-in, default OFF for safe rollout. */
796
799
  function inlineCrushEnabled() {
797
800
  const v = process.env.GREPRAG_OPENCODE_CRUSH;
@@ -1115,6 +1118,7 @@ const GrepRAGMemoryPlugin = async (ctx) => {
1115
1118
  const stats = (0, opencode_plugin_crush_1.crushMessages)(msgs, {
1116
1119
  stash: makeCcrStash(),
1117
1120
  thresholdBytes: Number.isFinite(thresholdEnv) && thresholdEnv > 0 ? thresholdEnv : undefined,
1121
+ seenHashes: _crushSeenHashes ?? (_crushSeenHashes = new Set()),
1118
1122
  });
1119
1123
  if (stats.partsCrushed > 0 || stats.partsReverted > 0) {
1120
1124
  dlog(`messages.transform: crushed=${stats.partsCrushed} reverted=${stats.partsReverted} ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greprag",
3
- "version": "5.74.18",
3
+ "version": "5.74.19",
4
4
  "description": "GrepRAG — agent memory for Claude Code, Codex, and OpenCode.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {