greprag 5.74.18 → 5.74.20

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,15 +281,37 @@ 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) {
291
288
  const parts = msg && typeof msg === 'object' ? msg.parts : null;
292
289
  if (!Array.isArray(parts))
293
290
  continue;
291
+ let lastToolName = '';
294
292
  for (const part of parts) {
295
293
  try {
296
- processPart(part, opts.stash, thresholdBytes, stats, opts.memo);
294
+ const p = part;
295
+ // Track the last tool_use command to skip crushing its result
296
+ if (p && typeof p === 'object' && (p.type === 'tool_use' || p.type === 'tool' && p.state === undefined)) {
297
+ const input = p.input;
298
+ const cmd = input && typeof input.command === 'string' ? input.command : '';
299
+ const name = typeof p.name === 'string' ? p.name : '';
300
+ lastToolName = cmd || name;
301
+ }
302
+ if (lastToolName && lastToolName.includes('greprag retrieve')) {
303
+ // Check if this is a tool_result for the greprag retrieve command
304
+ if (p && typeof p === 'object' && p.type === 'tool') {
305
+ stats.partsSkipped++;
306
+ continue;
307
+ }
308
+ // Also skip text parts immediately following (optional)
309
+ if (p && typeof p === 'object' && p.type === 'tool_result' && typeof p.content === 'string') {
310
+ stats.partsSkipped++;
311
+ continue;
312
+ }
313
+ }
314
+ processPart(part, opts.stash, thresholdBytes, stats, opts.memo, seenHashes);
297
315
  }
298
316
  catch {
299
317
  // per-part swallow — a crush/stash failure must never break the turn
@@ -22,18 +22,18 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
 
25
- // ../greprag/packages/cli/src/opencode-plugin.ts
25
+ // src/opencode-plugin.ts
26
26
  var crypto5 = __toESM(require("crypto"));
27
27
  var fs7 = __toESM(require("fs"));
28
28
  var os2 = __toESM(require("os"));
29
29
  var path7 = __toESM(require("path"));
30
30
 
31
- // ../greprag/packages/cli/src/opencode-plugin-helpers.ts
31
+ // src/opencode-plugin-helpers.ts
32
32
  var crypto = __toESM(require("crypto"));
33
33
  var fs = __toESM(require("fs"));
34
34
  var path = __toESM(require("path"));
35
35
 
36
- // ../greprag/packages/cli/src/proc.ts
36
+ // src/proc.ts
37
37
  var import_child_process = require("child_process");
38
38
  function safeExecSync(command, options) {
39
39
  return (0, import_child_process.execSync)(command, { windowsHide: true, ...options });
@@ -45,7 +45,7 @@ function safeSpawn(command, argsOrOptions, maybeOptions) {
45
45
  return (0, import_child_process.spawn)(command, { windowsHide: true, ...argsOrOptions });
46
46
  }
47
47
 
48
- // ../greprag/packages/cli/src/opencode-plugin-helpers.ts
48
+ // src/opencode-plugin-helpers.ts
49
49
  var HOME = process.env.HOME || process.env.USERPROFILE || "";
50
50
  function readAnchorFile(filePath) {
51
51
  try {
@@ -272,7 +272,7 @@ async function buildRecapBody(apiUrl, apiKey, anchor, now = /* @__PURE__ */ new
272
272
  return parts.join("\n");
273
273
  }
274
274
 
275
- // ../greprag/packages/cli/src/crush/crush-types.ts
275
+ // src/crush/crush-types.ts
276
276
  function estimateTokens(text) {
277
277
  return Math.ceil(text.length / 4);
278
278
  }
@@ -292,7 +292,7 @@ function makePassthrough(content, sourceType, reason, units = 0) {
292
292
  };
293
293
  }
294
294
 
295
- // ../greprag/packages/cli/src/crush/adaptive-sizer.ts
295
+ // src/crush/adaptive-sizer.ts
296
296
  function fnv1a(s, seed) {
297
297
  let h = (2166136261 ^ seed) >>> 0;
298
298
  for (let i = 0; i < s.length; i++) {
@@ -394,7 +394,7 @@ function computeOptimalK(items, bias, minK, maxK) {
394
394
  return Math.max(minK, Math.min(k, effectiveMax));
395
395
  }
396
396
 
397
- // ../greprag/packages/cli/src/crush/crush-keywords.ts
397
+ // src/crush/crush-keywords.ts
398
398
  var ERROR_KEYWORDS = [
399
399
  "error",
400
400
  "exception",
@@ -467,7 +467,7 @@ function classifyImportance(line) {
467
467
  return null;
468
468
  }
469
469
 
470
- // ../greprag/packages/cli/src/crush/search-compressor.ts
470
+ // src/crush/search-compressor.ts
471
471
  var DEFAULT_SEARCH_CONFIG = {
472
472
  maxMatchesPerFile: 5,
473
473
  alwaysKeepFirst: true,
@@ -630,7 +630,7 @@ function crushSearch(content, query = "", config = {}) {
630
630
  };
631
631
  }
632
632
 
633
- // ../greprag/packages/cli/src/crush/log-compressor.ts
633
+ // src/crush/log-compressor.ts
634
634
  var DEFAULT_LOG_CONFIG = {
635
635
  maxErrors: 10,
636
636
  errorContextLines: 3,
@@ -941,7 +941,7 @@ function crushLog(content, config = {}) {
941
941
  };
942
942
  }
943
943
 
944
- // ../greprag/packages/cli/src/crush/json-detectors.ts
944
+ // src/crush/json-detectors.ts
945
945
  function classifyArray(items) {
946
946
  if (items.length === 0)
947
947
  return "empty";
@@ -1101,7 +1101,7 @@ function detectErrorItems(items) {
1101
1101
  return out;
1102
1102
  }
1103
1103
 
1104
- // ../greprag/packages/cli/src/crush/json-crusher.ts
1104
+ // src/crush/json-crusher.ts
1105
1105
  var DEFAULT_JSON_CONFIG = {
1106
1106
  minItemsToAnalyze: 10,
1107
1107
  maxItemsAfterCrush: 15,
@@ -1312,7 +1312,7 @@ function crushJson(content, config = {}) {
1312
1312
  };
1313
1313
  }
1314
1314
 
1315
- // ../greprag/packages/cli/src/crush/code-compressor.ts
1315
+ // src/crush/code-compressor.ts
1316
1316
  var DEFAULT_CODE_CONFIG = {
1317
1317
  minLines: 8,
1318
1318
  maxBodyLines: 3
@@ -1599,7 +1599,8 @@ function crushCode(content, config = {}) {
1599
1599
  };
1600
1600
  }
1601
1601
 
1602
- // ../greprag/packages/cli/src/opencode-plugin-crush.ts
1602
+ // 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,15 +1733,34 @@ 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) {
1748
1740
  const parts = msg && typeof msg === "object" ? msg.parts : null;
1749
1741
  if (!Array.isArray(parts))
1750
1742
  continue;
1743
+ let lastToolName = "";
1751
1744
  for (const part of parts) {
1752
1745
  try {
1753
- processPart(part, opts.stash, thresholdBytes, stats, opts.memo);
1746
+ const p = part;
1747
+ if (p && typeof p === "object" && (p.type === "tool_use" || p.type === "tool" && p.state === void 0)) {
1748
+ const input = p.input;
1749
+ const cmd = input && typeof input.command === "string" ? input.command : "";
1750
+ const name = typeof p.name === "string" ? p.name : "";
1751
+ lastToolName = cmd || name;
1752
+ }
1753
+ if (lastToolName && lastToolName.includes("greprag retrieve")) {
1754
+ if (p && typeof p === "object" && p.type === "tool") {
1755
+ stats.partsSkipped++;
1756
+ continue;
1757
+ }
1758
+ if (p && typeof p === "object" && p.type === "tool_result" && typeof p.content === "string") {
1759
+ stats.partsSkipped++;
1760
+ continue;
1761
+ }
1762
+ }
1763
+ processPart(part, opts.stash, thresholdBytes, stats, opts.memo, seenHashes);
1754
1764
  } catch {
1755
1765
  stats.partsSkipped++;
1756
1766
  }
@@ -1759,7 +1769,7 @@ function crushMessages(messages, opts) {
1759
1769
  return stats;
1760
1770
  }
1761
1771
 
1762
- // ../greprag/packages/cli/src/commands/os-primer-reminder.ts
1772
+ // src/commands/os-primer-reminder.ts
1763
1773
  function buildOsPrimer(env) {
1764
1774
  const spawnEntry = env?.platform === "codex" ? "codex-chip-spawn" : env?.platform === "opencode" ? "chip-bootloader" : "chip-spawn";
1765
1775
  return [
@@ -1779,7 +1789,7 @@ var osPrimerModule = {
1779
1789
  reminder: () => null
1780
1790
  };
1781
1791
 
1782
- // ../greprag/packages/cli/src/session-id.ts
1792
+ // src/session-id.ts
1783
1793
  function armMonitorCommand(short, ownerPid, assistant = false, mechanic = false) {
1784
1794
  const owner = ownerPid ? ` --owner-pid ${ownerPid}` : "";
1785
1795
  const role = mechanic ? " --mechanic" : assistant ? " --assistant" : "";
@@ -1787,7 +1797,7 @@ function armMonitorCommand(short, ownerPid, assistant = false, mechanic = false)
1787
1797
  return `while true; do ${watch}; case $? in 0|64) break;; esac; sleep 1; done`;
1788
1798
  }
1789
1799
 
1790
- // ../greprag/packages/cli/src/commands/inbox-primer-reminder.ts
1800
+ // src/commands/inbox-primer-reminder.ts
1791
1801
  function buildInboxPrimer(env) {
1792
1802
  const codex = env.platform === "codex";
1793
1803
  const opencode = env.platform === "opencode";
@@ -1825,7 +1835,7 @@ var inboxPrimerModule = {
1825
1835
  reminder: () => null
1826
1836
  };
1827
1837
 
1828
- // ../greprag/packages/cli/src/commands/load-primer-reminder.ts
1838
+ // src/commands/load-primer-reminder.ts
1829
1839
  var LOAD_PRIMER = [
1830
1840
  "[greprag load \u2014 on-demand doctrine. greprag ships its own methods inside the CLI; you never need a `.md` file or a CLAUDE.md entry to use them.]",
1831
1841
  "\u2022 `greprag load` \u2192 browse the catalog (every doctrine entry + its one-line purpose).",
@@ -1877,7 +1887,7 @@ var codexChipSpawnPointerModule = {
1877
1887
  reminder: () => null
1878
1888
  };
1879
1889
 
1880
- // ../greprag/packages/cli/src/commands/email-primer-reminder.ts
1890
+ // src/commands/email-primer-reminder.ts
1881
1891
  var EMAIL_PRIMER = [
1882
1892
  "[greprag email \u2014 the agent's REAL mailbox (actual inbound + outbound SMTP), distinct from `greprag send` (internal session mesh, never real email).]",
1883
1893
  '\u2022 SEND real mail: `greprag email send --to <addr> --subject "s" (--body "t" | --body-file <f|->)` \u2014 from your <handle>@greprag.com. Flags: --html-file (rich body) \xB7 --attach (repeatable) \xB7 --cc/--bcc \xB7 --reply-to \xB7 --from-name \xB7 --from <your-own-handle> (server rejects any address that isn\'t yours \u2014 no forging).',
@@ -1894,7 +1904,7 @@ var emailPrimerModule = {
1894
1904
  reminder: () => null
1895
1905
  };
1896
1906
 
1897
- // ../greprag/packages/cli/src/commands/corpus-reminder.ts
1907
+ // src/commands/corpus-reminder.ts
1898
1908
  function buildCorpusAnnounce(names) {
1899
1909
  if (!names || names.length === 0)
1900
1910
  return null;
@@ -1914,7 +1924,7 @@ var corpusAnnounceModule = {
1914
1924
  reminder: () => null
1915
1925
  };
1916
1926
 
1917
- // ../greprag/packages/cli/src/commands/doc-pointer-reminder.ts
1927
+ // src/commands/doc-pointer-reminder.ts
1918
1928
  var ANNOUNCE_CAP = 20;
1919
1929
  function buildDocPointerAnnounce(pointers) {
1920
1930
  if (!pointers || pointers.length === 0)
@@ -1946,7 +1956,7 @@ var docPointerAnnounceModule = {
1946
1956
  reminder: () => null
1947
1957
  };
1948
1958
 
1949
- // ../greprag/packages/cli/src/commands/setup-reminder.ts
1959
+ // src/commands/setup-reminder.ts
1950
1960
  function setupDetect(env) {
1951
1961
  return env.setupWarning ? { tier: "nudge" } : { tier: "silent" };
1952
1962
  }
@@ -1957,7 +1967,7 @@ var setupWarningModule = {
1957
1967
  reminder: () => null
1958
1968
  };
1959
1969
 
1960
- // ../greprag/packages/cli/src/commands/assistant-reminder.ts
1970
+ // src/commands/assistant-reminder.ts
1961
1971
  function assistantDetect(env) {
1962
1972
  return env.assistantDoctrine ? { tier: "ambient" } : { tier: "silent" };
1963
1973
  }
@@ -1968,7 +1978,7 @@ var assistantDoctrineModule = {
1968
1978
  reminder: () => null
1969
1979
  };
1970
1980
 
1971
- // ../greprag/packages/cli/src/commands/memory-reflex.ts
1981
+ // src/commands/memory-reflex.ts
1972
1982
  function buildMemoryAnnounce() {
1973
1983
  return [
1974
1984
  "[greprag memory \u2014 this project remembers. Every past session (decisions, bugs, what was discussed, who did what) is captured and searchable.]",
@@ -1982,7 +1992,7 @@ var memoryPrimerModule = {
1982
1992
  reminder: () => null
1983
1993
  };
1984
1994
 
1985
- // ../greprag/packages/cli/src/commands/arm-reminder.ts
1995
+ // src/commands/arm-reminder.ts
1986
1996
  function armDetect(env) {
1987
1997
  if (env.armed)
1988
1998
  return { tier: "silent" };
@@ -2012,11 +2022,11 @@ var watcherArmModule = {
2012
2022
  reminder: (d, env) => buildArmReminder(d, env)
2013
2023
  };
2014
2024
 
2015
- // ../greprag/packages/cli/src/commands/state-trigger.ts
2025
+ // src/commands/state-trigger.ts
2016
2026
  var STRESS_ELEVATED = 1;
2017
2027
  var STRESS_HIGH = 2;
2018
2028
 
2019
- // ../greprag/packages/cli/src/commands/friction-reminder.ts
2029
+ // src/commands/friction-reminder.ts
2020
2030
  function quoteForCommand(value) {
2021
2031
  return value.replace(/[\r\n]+/g, " ").replace(/"/g, '\\"').trim();
2022
2032
  }
@@ -2081,7 +2091,7 @@ var mechanicFrictionModule = {
2081
2091
  }
2082
2092
  };
2083
2093
 
2084
- // ../greprag/packages/cli/src/commands/coordinate-gate.ts
2094
+ // src/commands/coordinate-gate.ts
2085
2095
  var RISKY_PATTERNS = [
2086
2096
  { kind: "merge", label: "gh pr merge", re: /^gh\s+pr\s+merge\b/ },
2087
2097
  { kind: "deploy", label: "wrangler deploy", re: /^(?:npx\s+)?wrangler\s+deploy\b/ },
@@ -2198,7 +2208,7 @@ function buildCoordinateDirective(trigger, peers, myShort, alias) {
2198
2208
  return `COORDINATE before ${trigger.label}: ${peers.length} peer${plural} live in this repo (${list}). Ping before proceeding. Codex peers: use codex_app.list_threads and codex_app.send_message_to_thread. GrepRAG watcher/cross-harness peers: greprag send "heads up, about to ${trigger.label}" --to ${handle}@greprag.com/<8hex> --from-session ${myShort}`;
2199
2209
  }
2200
2210
 
2201
- // ../greprag/packages/cli/src/commands/collision-reminder.ts
2211
+ // src/commands/collision-reminder.ts
2202
2212
  var collisionMatchModule = {
2203
2213
  id: "collision",
2204
2214
  source: "command",
@@ -2223,7 +2233,7 @@ var collisionMatchModule = {
2223
2233
  }
2224
2234
  };
2225
2235
 
2226
- // ../greprag/packages/cli/src/commands/version-reminder.ts
2236
+ // src/commands/version-reminder.ts
2227
2237
  function buildVersionAnnounce(current, latest) {
2228
2238
  return [
2229
2239
  `[greprag \u2014 a new release is out: v${latest} (you are on v${current}).]`,
@@ -2264,7 +2274,7 @@ var versionUpgradeModule = {
2264
2274
  reminder: () => null
2265
2275
  };
2266
2276
 
2267
- // ../greprag/packages/cli/src/commands/enrichment-health-reminder.ts
2277
+ // src/commands/enrichment-health-reminder.ts
2268
2278
  function buildEnrichmentHealthAnnounce(down) {
2269
2279
  if (!down || !down.errorClass)
2270
2280
  return null;
@@ -2283,7 +2293,7 @@ var enrichmentHealthModule = {
2283
2293
  reminder: () => null
2284
2294
  };
2285
2295
 
2286
- // ../greprag/packages/cli/src/commands/skill-gain-reminder.ts
2296
+ // src/commands/skill-gain-reminder.ts
2287
2297
  function buildSkillGainAnnounce(gains) {
2288
2298
  if (!gains || gains.landed.length === 0)
2289
2299
  return null;
@@ -2302,7 +2312,7 @@ var skillGainAnnounceModule = {
2302
2312
  reminder: () => null
2303
2313
  };
2304
2314
 
2305
- // ../greprag/packages/cli/src/commands/skill-mirror-reminder.ts
2315
+ // src/commands/skill-mirror-reminder.ts
2306
2316
  var SKILL_ACTIVATION_MANIFEST_MAX_CHARS = 64e3;
2307
2317
  function renderEntry(skill) {
2308
2318
  const description = skill.triggerDescription.trim().replace(/\r\n?/g, "\n");
@@ -2355,7 +2365,7 @@ var skillMirrorAnnounceModule = {
2355
2365
  reminder: () => null
2356
2366
  };
2357
2367
 
2358
- // ../greprag/packages/cli/src/commands/procedure-reminder.ts
2368
+ // src/commands/procedure-reminder.ts
2359
2369
  function buildProcedureAnnounce(lines) {
2360
2370
  const active = (lines ?? []).map((s) => s.trim()).filter(Boolean);
2361
2371
  if (!active.length)
@@ -2373,7 +2383,7 @@ var procedureAnnounceModule = {
2373
2383
  reminder: () => null
2374
2384
  };
2375
2385
 
2376
- // ../greprag/packages/cli/src/commands/loadout-reminder.ts
2386
+ // src/commands/loadout-reminder.ts
2377
2387
  function matchLoadouts(promptText, equipped) {
2378
2388
  const prompt = (promptText || "").toLowerCase();
2379
2389
  if (!prompt.trim() || !equipped || equipped.length === 0)
@@ -2412,7 +2422,7 @@ var loadoutRegistrarModule = {
2412
2422
  reminder: (d) => buildLoadoutReminder(d.detail?.matched || [])
2413
2423
  };
2414
2424
 
2415
- // ../greprag/packages/cli/src/commands/delivery-reminder.ts
2425
+ // src/commands/delivery-reminder.ts
2416
2426
  function coordinationLine(platform) {
2417
2427
  if (platform === "codex") {
2418
2428
  return "Codex: at the merge/deploy gate, call codex_app.list_threads unfiltered, filter to peers in the same repo, then use codex_app.send_message_to_thread once for ready or overlapping work. Delivery owner: rename this task `DEPLOY: <existing title>`; transfer the prefix on handoff; remove the prefix when delivery is done.";
@@ -2444,7 +2454,7 @@ var deliveryControlModule = {
2444
2454
  reminder: () => null
2445
2455
  };
2446
2456
 
2447
- // ../greprag/packages/cli/src/commands/reminder-registry.ts
2457
+ // src/commands/reminder-registry.ts
2448
2458
  var REGISTRY = [
2449
2459
  osPrimerModule,
2450
2460
  // grepragOS constitution — the frame every other primer hangs off; boots FIRST
@@ -2539,7 +2549,7 @@ function collectAnnounces(env, registry = REGISTRY) {
2539
2549
  return out;
2540
2550
  }
2541
2551
 
2542
- // ../greprag/packages/cli/src/commands/opencode-interrupt.ts
2552
+ // src/commands/opencode-interrupt.ts
2543
2553
  var opencodeRegistry = harnessModules("opencode");
2544
2554
  var OPENCODE_QUOTA_WAITLIST_ANNOUNCE = [
2545
2555
  "[GrepRAG quota decision rule]",
@@ -2578,11 +2588,11 @@ function getOpenCodeReminders(env) {
2578
2588
  return collectReminders(env, opencodeRegistry);
2579
2589
  }
2580
2590
 
2581
- // ../greprag/packages/cli/src/procedure.ts
2591
+ // src/procedure.ts
2582
2592
  var path2 = __toESM(require("path"));
2583
2593
  var fs2 = __toESM(require("fs"));
2584
2594
 
2585
- // ../greprag/packages/cli/src/delivery-lifecycle.ts
2595
+ // src/delivery-lifecycle.ts
2586
2596
  var DELIVERY_LIFECYCLE_VERBS = [
2587
2597
  "commit",
2588
2598
  "merge",
@@ -2595,7 +2605,7 @@ function isDeliveryLifecycleVerb(verb) {
2595
2605
  return DELIVERY_LIFECYCLE_SET.has((verb || "").trim().toLowerCase());
2596
2606
  }
2597
2607
 
2598
- // ../greprag/packages/cli/src/procedure.ts
2608
+ // src/procedure.ts
2599
2609
  var PROCEDURE_STORE_VERSION = "2";
2600
2610
  function stateDir() {
2601
2611
  const home = process.env.HOME || process.env.USERPROFILE || "";
@@ -2836,10 +2846,10 @@ function activeProcedureAnnounces(store) {
2836
2846
  return lines;
2837
2847
  }
2838
2848
 
2839
- // ../greprag/packages/cli/src/procedure-runtime.ts
2849
+ // src/procedure-runtime.ts
2840
2850
  var crypto4 = __toESM(require("crypto"));
2841
2851
 
2842
- // ../greprag/packages/cli/src/project-anchor.ts
2852
+ // src/project-anchor.ts
2843
2853
  var path3 = __toESM(require("path"));
2844
2854
  var fs3 = __toESM(require("fs"));
2845
2855
  var crypto2 = __toESM(require("crypto"));
@@ -3021,7 +3031,7 @@ function readAnchor2(cwd) {
3021
3031
  };
3022
3032
  }
3023
3033
 
3024
- // ../greprag/packages/cli/src/procedure-watch.ts
3034
+ // src/procedure-watch.ts
3025
3035
  var path4 = __toESM(require("path"));
3026
3036
  var fs4 = __toESM(require("fs"));
3027
3037
  var TIER1_LEARN_TRIGGERS = [
@@ -3082,7 +3092,7 @@ function openWatchIfIdle(projectId, verb, phase, shadowRunId) {
3082
3092
  return true;
3083
3093
  }
3084
3094
 
3085
- // ../greprag/packages/cli/src/procedure-shadow.ts
3095
+ // src/procedure-shadow.ts
3086
3096
  var crypto3 = __toESM(require("crypto"));
3087
3097
  var fs5 = __toESM(require("fs"));
3088
3098
  var path5 = __toESM(require("path"));
@@ -3148,7 +3158,7 @@ function recordMissingLifecycleShadowTrigger(params) {
3148
3158
  });
3149
3159
  }
3150
3160
 
3151
- // ../greprag/packages/cli/src/procedure-runtime.ts
3161
+ // src/procedure-runtime.ts
3152
3162
  function extractLatestOpenCodeProcedurePrompt(messages) {
3153
3163
  let userCount = 0;
3154
3164
  let latest = null;
@@ -3234,7 +3244,7 @@ function runProcedurePromptCheck(params) {
3234
3244
  }
3235
3245
  }
3236
3246
 
3237
- // ../greprag/packages/cli/src/archive/glyph-font.ts
3247
+ // src/archive/glyph-font.ts
3238
3248
  var GLYPH_W = 8;
3239
3249
  var GLYPH_H = 8;
3240
3250
  var FIRST_CODE = 32;
@@ -3358,7 +3368,7 @@ function signatureForRows(bytes) {
3358
3368
  return bytes.map((b) => (b & 255).toString(16).padStart(2, "0")).join("");
3359
3369
  }
3360
3370
 
3361
- // ../greprag/packages/cli/src/archive/png-codec.ts
3371
+ // src/archive/png-codec.ts
3362
3372
  var zlib = __toESM(require("zlib"));
3363
3373
  var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
3364
3374
  var crcTable = null;
@@ -3500,7 +3510,7 @@ function decodePng(buf) {
3500
3510
  return { width, height, pixels };
3501
3511
  }
3502
3512
 
3503
- // ../greprag/packages/cli/src/archive/page-renderer.ts
3513
+ // src/archive/page-renderer.ts
3504
3514
  var PAGE_SIZE = 1568;
3505
3515
  var CELL_W = GLYPH_W;
3506
3516
  var CELL_H = 10;
@@ -3626,7 +3636,7 @@ function verifyRoundTrip(page) {
3626
3636
  return `length mismatch: rendered ${a.length} decoded ${b.length}`;
3627
3637
  }
3628
3638
 
3629
- // ../greprag/packages/cli/src/opencode-plugin-png.ts
3639
+ // src/opencode-plugin-png.ts
3630
3640
  var CCR_MARKER_HASH_LEN2 = 16;
3631
3641
  function ccrPngMarker(hash) {
3632
3642
  return `<<ccr:${hash.slice(0, CCR_MARKER_HASH_LEN2)}>>`;
@@ -3816,7 +3826,7 @@ function recodeMessagesToPng(messages, opts) {
3816
3826
  return stats;
3817
3827
  }
3818
3828
 
3819
- // ../greprag/packages/cli/src/skill-activation-manifest.ts
3829
+ // src/skill-activation-manifest.ts
3820
3830
  var fs6 = __toESM(require("fs"));
3821
3831
  var path6 = __toESM(require("path"));
3822
3832
  var MAX_NATIVE_SKILL_FILES = 1500;
@@ -3917,7 +3927,7 @@ function activationFromApiRows(params) {
3917
3927
  return buildMirroredSkillActivation(entries, readNativeSkillNames(params));
3918
3928
  }
3919
3929
 
3920
- // ../greprag/packages/cli/src/opencode-plugin.ts
3930
+ // src/opencode-plugin.ts
3921
3931
  var DEBUG_LOG_PATH = path7.join(os2.homedir(), ".greprag", "opencode-plugin-debug.log");
3922
3932
  var _debugLogReady = false;
3923
3933
  function dlogInit() {
@@ -4498,6 +4508,7 @@ function grepragBinCached() {
4498
4508
  return _cachedGrepragBin;
4499
4509
  }
4500
4510
  var _crushShapeLogged = false;
4511
+ var _crushSeenHashes;
4501
4512
  function inlineCrushEnabled() {
4502
4513
  const v = process.env.GREPRAG_OPENCODE_CRUSH;
4503
4514
  return v === "true" || v === "1";
@@ -4740,7 +4751,8 @@ ALWAYS pass \`--from-session ${sid82}\` when you run \`greprag send\`, so recipi
4740
4751
  const thresholdEnv = parseInt(process.env.GREPRAG_OPENCODE_CRUSH_MIN_BYTES || "", 10);
4741
4752
  const stats = crushMessages(msgs, {
4742
4753
  stash: makeCcrStash(),
4743
- thresholdBytes: Number.isFinite(thresholdEnv) && thresholdEnv > 0 ? thresholdEnv : void 0
4754
+ thresholdBytes: Number.isFinite(thresholdEnv) && thresholdEnv > 0 ? thresholdEnv : void 0,
4755
+ seenHashes: _crushSeenHashes ?? (_crushSeenHashes = /* @__PURE__ */ new Set())
4744
4756
  });
4745
4757
  if (stats.partsCrushed > 0 || stats.partsReverted > 0) {
4746
4758
  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.20",
4
4
  "description": "GrepRAG — agent memory for Claude Code, Codex, and OpenCode.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {