greprag 5.74.17 → 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,9 +184,26 @@ 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 ----------------------------------------------
191
+ /** Resolve the bash command string from a tool part, regardless of whether
192
+ * the part is a tool_use (carries .input.command) or a tool_result (may
193
+ * carry .name, .tool, or .toolName). Returns an empty string when the
194
+ * command cannot be resolved. */
195
+ function resolveRetrieveCommand(p) {
196
+ const input = p.input;
197
+ if (input && typeof input.command === 'string')
198
+ return input.command;
199
+ const toolName = (typeof p.name === 'string' && p.name) ||
200
+ (typeof p.tool === 'string' && p.tool) ||
201
+ (typeof p.toolName === 'string' && p.toolName);
202
+ // opencode may carry the full command string in .name / .tool
203
+ if (toolName)
204
+ return toolName;
205
+ return '';
206
+ }
183
207
  /** Apply crushField to one object's string field, accounting stats and
184
208
  * mutating in place on a real crush. When a `memo` is supplied, the heavy
185
209
  * crushField path is skipped for content already seen this session — the
@@ -187,7 +211,7 @@ function crushField(content, stash, thresholdBytes = exports.DEFAULT_THRESHOLD_B
187
211
  * ORIGINAL content, and crush is deterministic, so the reused value is exactly
188
212
  * what crushField would recompute; the original was already stashed on the
189
213
  * first (miss) crush, so skipping re-stash is safe for retrieve. */
190
- function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
214
+ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo, seenHashes) {
191
215
  const content = obj[key];
192
216
  if (typeof content !== 'string')
193
217
  return;
@@ -209,13 +233,13 @@ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
209
233
  stats.memoHits++;
210
234
  }
211
235
  else {
212
- replacement = crushField(content, stash, thresholdBytes);
236
+ replacement = crushField(content, stash, thresholdBytes, seenHashes);
213
237
  stats.crushFieldCalls++;
214
238
  memo.set(k, replacement);
215
239
  }
216
240
  }
217
241
  else {
218
- replacement = crushField(content, stash, thresholdBytes);
242
+ replacement = crushField(content, stash, thresholdBytes, seenHashes);
219
243
  stats.crushFieldCalls++;
220
244
  }
221
245
  if (replacement === null) {
@@ -231,28 +255,18 @@ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
231
255
  * - text part → `.text`
232
256
  * - tool part → `.state.output` (only when state.status === 'completed';
233
257
  * an 'error' state's `.error` is precious — left alone). */
234
- function processPart(part, stash, thresholdBytes, stats, memo) {
258
+ function processPart(part, stash, thresholdBytes, stats, memo, seenHashes) {
235
259
  if (!part || typeof part !== 'object')
236
260
  return;
237
261
  const p = part;
238
262
  if (p.type === 'text' && typeof p.text === 'string') {
239
- crushAndApply(p, 'text', stash, thresholdBytes, stats, memo);
263
+ crushAndApply(p, 'text', stash, thresholdBytes, stats, memo, seenHashes);
240
264
  return;
241
265
  }
242
266
  if (p.type === 'tool' && p.state && typeof p.state === 'object') {
243
267
  const state = p.state;
244
268
  if (state.status === 'completed' && typeof state.output === 'string') {
245
- // Skip re-crushing output of greprag retrieve the original was
246
- // already stashed once; crushing it again creates an infinite
247
- // marker→retrieve→same marker loop.
248
- const input = p.input;
249
- const cmd = input && typeof input.command === 'string' ? input.command : '';
250
- if (cmd.includes('greprag retrieve')) {
251
- stats.partsSkipped++;
252
- }
253
- else {
254
- crushAndApply(state, 'output', stash, thresholdBytes, stats, memo);
255
- }
269
+ crushAndApply(state, 'output', stash, thresholdBytes, stats, memo, seenHashes);
256
270
  }
257
271
  }
258
272
  }
@@ -267,6 +281,7 @@ function crushMessages(messages, opts) {
267
281
  crushFieldCalls: 0, memoHits: 0,
268
282
  };
269
283
  const thresholdBytes = opts.thresholdBytes ?? exports.DEFAULT_THRESHOLD_BYTES;
284
+ const seenHashes = opts.seenHashes ?? new Set();
270
285
  if (!Array.isArray(messages))
271
286
  return stats;
272
287
  for (const msg of messages) {
@@ -275,7 +290,7 @@ function crushMessages(messages, opts) {
275
290
  continue;
276
291
  for (const part of parts) {
277
292
  try {
278
- processPart(part, opts.stash, thresholdBytes, stats, opts.memo);
293
+ processPart(part, opts.stash, thresholdBytes, stats, opts.memo, seenHashes);
279
294
  }
280
295
  catch {
281
296
  // 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
- // src/opencode-plugin.ts
25
+ // ../greprag/packages/cli/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
- // src/opencode-plugin-helpers.ts
31
+ // ../greprag/packages/cli/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
- // src/proc.ts
36
+ // ../greprag/packages/cli/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
- // src/opencode-plugin-helpers.ts
48
+ // ../greprag/packages/cli/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
- // src/crush/crush-types.ts
275
+ // ../greprag/packages/cli/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
- // src/crush/adaptive-sizer.ts
295
+ // ../greprag/packages/cli/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
- // src/crush/crush-keywords.ts
397
+ // ../greprag/packages/cli/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
- // src/crush/search-compressor.ts
470
+ // ../greprag/packages/cli/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
- // src/crush/log-compressor.ts
633
+ // ../greprag/packages/cli/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
- // src/crush/json-detectors.ts
944
+ // ../greprag/packages/cli/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
- // src/crush/json-crusher.ts
1104
+ // ../greprag/packages/cli/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
- // src/crush/code-compressor.ts
1315
+ // ../greprag/packages/cli/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
- // src/opencode-plugin-crush.ts
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,10 +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 crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
1672
+ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo, seenHashes) {
1668
1673
  const content = obj[key];
1669
1674
  if (typeof content !== "string")
1670
1675
  return;
@@ -1685,12 +1690,12 @@ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
1685
1690
  replacement = memo.get(k) ?? null;
1686
1691
  stats.memoHits++;
1687
1692
  } else {
1688
- replacement = crushField(content, stash, thresholdBytes);
1693
+ replacement = crushField(content, stash, thresholdBytes, seenHashes);
1689
1694
  stats.crushFieldCalls++;
1690
1695
  memo.set(k, replacement);
1691
1696
  }
1692
1697
  } else {
1693
- replacement = crushField(content, stash, thresholdBytes);
1698
+ replacement = crushField(content, stash, thresholdBytes, seenHashes);
1694
1699
  stats.crushFieldCalls++;
1695
1700
  }
1696
1701
  if (replacement === null) {
@@ -1702,24 +1707,18 @@ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
1702
1707
  stats.partsCrushed++;
1703
1708
  stats.bytesAfter += Buffer.byteLength(replacement, "utf8");
1704
1709
  }
1705
- function processPart(part, stash, thresholdBytes, stats, memo) {
1710
+ function processPart(part, stash, thresholdBytes, stats, memo, seenHashes) {
1706
1711
  if (!part || typeof part !== "object")
1707
1712
  return;
1708
1713
  const p = part;
1709
1714
  if (p.type === "text" && typeof p.text === "string") {
1710
- crushAndApply(p, "text", stash, thresholdBytes, stats, memo);
1715
+ crushAndApply(p, "text", stash, thresholdBytes, stats, memo, seenHashes);
1711
1716
  return;
1712
1717
  }
1713
1718
  if (p.type === "tool" && p.state && typeof p.state === "object") {
1714
1719
  const state = p.state;
1715
1720
  if (state.status === "completed" && typeof state.output === "string") {
1716
- const input = p.input;
1717
- const cmd = input && typeof input.command === "string" ? input.command : "";
1718
- if (cmd.includes("greprag retrieve")) {
1719
- stats.partsSkipped++;
1720
- } else {
1721
- crushAndApply(state, "output", stash, thresholdBytes, stats, memo);
1722
- }
1721
+ crushAndApply(state, "output", stash, thresholdBytes, stats, memo, seenHashes);
1723
1722
  }
1724
1723
  }
1725
1724
  }
@@ -1734,6 +1733,7 @@ function crushMessages(messages, opts) {
1734
1733
  memoHits: 0
1735
1734
  };
1736
1735
  const thresholdBytes = opts.thresholdBytes ?? DEFAULT_THRESHOLD_BYTES;
1736
+ const seenHashes = opts.seenHashes ?? /* @__PURE__ */ new Set();
1737
1737
  if (!Array.isArray(messages))
1738
1738
  return stats;
1739
1739
  for (const msg of messages) {
@@ -1742,7 +1742,7 @@ function crushMessages(messages, opts) {
1742
1742
  continue;
1743
1743
  for (const part of parts) {
1744
1744
  try {
1745
- processPart(part, opts.stash, thresholdBytes, stats, opts.memo);
1745
+ processPart(part, opts.stash, thresholdBytes, stats, opts.memo, seenHashes);
1746
1746
  } catch {
1747
1747
  stats.partsSkipped++;
1748
1748
  }
@@ -1751,7 +1751,7 @@ function crushMessages(messages, opts) {
1751
1751
  return stats;
1752
1752
  }
1753
1753
 
1754
- // src/commands/os-primer-reminder.ts
1754
+ // ../greprag/packages/cli/src/commands/os-primer-reminder.ts
1755
1755
  function buildOsPrimer(env) {
1756
1756
  const spawnEntry = env?.platform === "codex" ? "codex-chip-spawn" : env?.platform === "opencode" ? "chip-bootloader" : "chip-spawn";
1757
1757
  return [
@@ -1771,7 +1771,7 @@ var osPrimerModule = {
1771
1771
  reminder: () => null
1772
1772
  };
1773
1773
 
1774
- // src/session-id.ts
1774
+ // ../greprag/packages/cli/src/session-id.ts
1775
1775
  function armMonitorCommand(short, ownerPid, assistant = false, mechanic = false) {
1776
1776
  const owner = ownerPid ? ` --owner-pid ${ownerPid}` : "";
1777
1777
  const role = mechanic ? " --mechanic" : assistant ? " --assistant" : "";
@@ -1779,7 +1779,7 @@ function armMonitorCommand(short, ownerPid, assistant = false, mechanic = false)
1779
1779
  return `while true; do ${watch}; case $? in 0|64) break;; esac; sleep 1; done`;
1780
1780
  }
1781
1781
 
1782
- // src/commands/inbox-primer-reminder.ts
1782
+ // ../greprag/packages/cli/src/commands/inbox-primer-reminder.ts
1783
1783
  function buildInboxPrimer(env) {
1784
1784
  const codex = env.platform === "codex";
1785
1785
  const opencode = env.platform === "opencode";
@@ -1817,7 +1817,7 @@ var inboxPrimerModule = {
1817
1817
  reminder: () => null
1818
1818
  };
1819
1819
 
1820
- // src/commands/load-primer-reminder.ts
1820
+ // ../greprag/packages/cli/src/commands/load-primer-reminder.ts
1821
1821
  var LOAD_PRIMER = [
1822
1822
  "[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.]",
1823
1823
  "\u2022 `greprag load` \u2192 browse the catalog (every doctrine entry + its one-line purpose).",
@@ -1869,7 +1869,7 @@ var codexChipSpawnPointerModule = {
1869
1869
  reminder: () => null
1870
1870
  };
1871
1871
 
1872
- // src/commands/email-primer-reminder.ts
1872
+ // ../greprag/packages/cli/src/commands/email-primer-reminder.ts
1873
1873
  var EMAIL_PRIMER = [
1874
1874
  "[greprag email \u2014 the agent's REAL mailbox (actual inbound + outbound SMTP), distinct from `greprag send` (internal session mesh, never real email).]",
1875
1875
  '\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).',
@@ -1886,7 +1886,7 @@ var emailPrimerModule = {
1886
1886
  reminder: () => null
1887
1887
  };
1888
1888
 
1889
- // src/commands/corpus-reminder.ts
1889
+ // ../greprag/packages/cli/src/commands/corpus-reminder.ts
1890
1890
  function buildCorpusAnnounce(names) {
1891
1891
  if (!names || names.length === 0)
1892
1892
  return null;
@@ -1906,7 +1906,7 @@ var corpusAnnounceModule = {
1906
1906
  reminder: () => null
1907
1907
  };
1908
1908
 
1909
- // src/commands/doc-pointer-reminder.ts
1909
+ // ../greprag/packages/cli/src/commands/doc-pointer-reminder.ts
1910
1910
  var ANNOUNCE_CAP = 20;
1911
1911
  function buildDocPointerAnnounce(pointers) {
1912
1912
  if (!pointers || pointers.length === 0)
@@ -1938,7 +1938,7 @@ var docPointerAnnounceModule = {
1938
1938
  reminder: () => null
1939
1939
  };
1940
1940
 
1941
- // src/commands/setup-reminder.ts
1941
+ // ../greprag/packages/cli/src/commands/setup-reminder.ts
1942
1942
  function setupDetect(env) {
1943
1943
  return env.setupWarning ? { tier: "nudge" } : { tier: "silent" };
1944
1944
  }
@@ -1949,7 +1949,7 @@ var setupWarningModule = {
1949
1949
  reminder: () => null
1950
1950
  };
1951
1951
 
1952
- // src/commands/assistant-reminder.ts
1952
+ // ../greprag/packages/cli/src/commands/assistant-reminder.ts
1953
1953
  function assistantDetect(env) {
1954
1954
  return env.assistantDoctrine ? { tier: "ambient" } : { tier: "silent" };
1955
1955
  }
@@ -1960,7 +1960,7 @@ var assistantDoctrineModule = {
1960
1960
  reminder: () => null
1961
1961
  };
1962
1962
 
1963
- // src/commands/memory-reflex.ts
1963
+ // ../greprag/packages/cli/src/commands/memory-reflex.ts
1964
1964
  function buildMemoryAnnounce() {
1965
1965
  return [
1966
1966
  "[greprag memory \u2014 this project remembers. Every past session (decisions, bugs, what was discussed, who did what) is captured and searchable.]",
@@ -1974,7 +1974,7 @@ var memoryPrimerModule = {
1974
1974
  reminder: () => null
1975
1975
  };
1976
1976
 
1977
- // src/commands/arm-reminder.ts
1977
+ // ../greprag/packages/cli/src/commands/arm-reminder.ts
1978
1978
  function armDetect(env) {
1979
1979
  if (env.armed)
1980
1980
  return { tier: "silent" };
@@ -2004,11 +2004,11 @@ var watcherArmModule = {
2004
2004
  reminder: (d, env) => buildArmReminder(d, env)
2005
2005
  };
2006
2006
 
2007
- // src/commands/state-trigger.ts
2007
+ // ../greprag/packages/cli/src/commands/state-trigger.ts
2008
2008
  var STRESS_ELEVATED = 1;
2009
2009
  var STRESS_HIGH = 2;
2010
2010
 
2011
- // src/commands/friction-reminder.ts
2011
+ // ../greprag/packages/cli/src/commands/friction-reminder.ts
2012
2012
  function quoteForCommand(value) {
2013
2013
  return value.replace(/[\r\n]+/g, " ").replace(/"/g, '\\"').trim();
2014
2014
  }
@@ -2073,7 +2073,7 @@ var mechanicFrictionModule = {
2073
2073
  }
2074
2074
  };
2075
2075
 
2076
- // src/commands/coordinate-gate.ts
2076
+ // ../greprag/packages/cli/src/commands/coordinate-gate.ts
2077
2077
  var RISKY_PATTERNS = [
2078
2078
  { kind: "merge", label: "gh pr merge", re: /^gh\s+pr\s+merge\b/ },
2079
2079
  { kind: "deploy", label: "wrangler deploy", re: /^(?:npx\s+)?wrangler\s+deploy\b/ },
@@ -2190,7 +2190,7 @@ function buildCoordinateDirective(trigger, peers, myShort, alias) {
2190
2190
  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}`;
2191
2191
  }
2192
2192
 
2193
- // src/commands/collision-reminder.ts
2193
+ // ../greprag/packages/cli/src/commands/collision-reminder.ts
2194
2194
  var collisionMatchModule = {
2195
2195
  id: "collision",
2196
2196
  source: "command",
@@ -2215,7 +2215,7 @@ var collisionMatchModule = {
2215
2215
  }
2216
2216
  };
2217
2217
 
2218
- // src/commands/version-reminder.ts
2218
+ // ../greprag/packages/cli/src/commands/version-reminder.ts
2219
2219
  function buildVersionAnnounce(current, latest) {
2220
2220
  return [
2221
2221
  `[greprag \u2014 a new release is out: v${latest} (you are on v${current}).]`,
@@ -2256,7 +2256,7 @@ var versionUpgradeModule = {
2256
2256
  reminder: () => null
2257
2257
  };
2258
2258
 
2259
- // src/commands/enrichment-health-reminder.ts
2259
+ // ../greprag/packages/cli/src/commands/enrichment-health-reminder.ts
2260
2260
  function buildEnrichmentHealthAnnounce(down) {
2261
2261
  if (!down || !down.errorClass)
2262
2262
  return null;
@@ -2275,7 +2275,7 @@ var enrichmentHealthModule = {
2275
2275
  reminder: () => null
2276
2276
  };
2277
2277
 
2278
- // src/commands/skill-gain-reminder.ts
2278
+ // ../greprag/packages/cli/src/commands/skill-gain-reminder.ts
2279
2279
  function buildSkillGainAnnounce(gains) {
2280
2280
  if (!gains || gains.landed.length === 0)
2281
2281
  return null;
@@ -2294,7 +2294,7 @@ var skillGainAnnounceModule = {
2294
2294
  reminder: () => null
2295
2295
  };
2296
2296
 
2297
- // src/commands/skill-mirror-reminder.ts
2297
+ // ../greprag/packages/cli/src/commands/skill-mirror-reminder.ts
2298
2298
  var SKILL_ACTIVATION_MANIFEST_MAX_CHARS = 64e3;
2299
2299
  function renderEntry(skill) {
2300
2300
  const description = skill.triggerDescription.trim().replace(/\r\n?/g, "\n");
@@ -2347,7 +2347,7 @@ var skillMirrorAnnounceModule = {
2347
2347
  reminder: () => null
2348
2348
  };
2349
2349
 
2350
- // src/commands/procedure-reminder.ts
2350
+ // ../greprag/packages/cli/src/commands/procedure-reminder.ts
2351
2351
  function buildProcedureAnnounce(lines) {
2352
2352
  const active = (lines ?? []).map((s) => s.trim()).filter(Boolean);
2353
2353
  if (!active.length)
@@ -2365,7 +2365,7 @@ var procedureAnnounceModule = {
2365
2365
  reminder: () => null
2366
2366
  };
2367
2367
 
2368
- // src/commands/loadout-reminder.ts
2368
+ // ../greprag/packages/cli/src/commands/loadout-reminder.ts
2369
2369
  function matchLoadouts(promptText, equipped) {
2370
2370
  const prompt = (promptText || "").toLowerCase();
2371
2371
  if (!prompt.trim() || !equipped || equipped.length === 0)
@@ -2404,7 +2404,7 @@ var loadoutRegistrarModule = {
2404
2404
  reminder: (d) => buildLoadoutReminder(d.detail?.matched || [])
2405
2405
  };
2406
2406
 
2407
- // src/commands/delivery-reminder.ts
2407
+ // ../greprag/packages/cli/src/commands/delivery-reminder.ts
2408
2408
  function coordinationLine(platform) {
2409
2409
  if (platform === "codex") {
2410
2410
  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.";
@@ -2436,7 +2436,7 @@ var deliveryControlModule = {
2436
2436
  reminder: () => null
2437
2437
  };
2438
2438
 
2439
- // src/commands/reminder-registry.ts
2439
+ // ../greprag/packages/cli/src/commands/reminder-registry.ts
2440
2440
  var REGISTRY = [
2441
2441
  osPrimerModule,
2442
2442
  // grepragOS constitution — the frame every other primer hangs off; boots FIRST
@@ -2531,7 +2531,7 @@ function collectAnnounces(env, registry = REGISTRY) {
2531
2531
  return out;
2532
2532
  }
2533
2533
 
2534
- // src/commands/opencode-interrupt.ts
2534
+ // ../greprag/packages/cli/src/commands/opencode-interrupt.ts
2535
2535
  var opencodeRegistry = harnessModules("opencode");
2536
2536
  var OPENCODE_QUOTA_WAITLIST_ANNOUNCE = [
2537
2537
  "[GrepRAG quota decision rule]",
@@ -2570,11 +2570,11 @@ function getOpenCodeReminders(env) {
2570
2570
  return collectReminders(env, opencodeRegistry);
2571
2571
  }
2572
2572
 
2573
- // src/procedure.ts
2573
+ // ../greprag/packages/cli/src/procedure.ts
2574
2574
  var path2 = __toESM(require("path"));
2575
2575
  var fs2 = __toESM(require("fs"));
2576
2576
 
2577
- // src/delivery-lifecycle.ts
2577
+ // ../greprag/packages/cli/src/delivery-lifecycle.ts
2578
2578
  var DELIVERY_LIFECYCLE_VERBS = [
2579
2579
  "commit",
2580
2580
  "merge",
@@ -2587,7 +2587,7 @@ function isDeliveryLifecycleVerb(verb) {
2587
2587
  return DELIVERY_LIFECYCLE_SET.has((verb || "").trim().toLowerCase());
2588
2588
  }
2589
2589
 
2590
- // src/procedure.ts
2590
+ // ../greprag/packages/cli/src/procedure.ts
2591
2591
  var PROCEDURE_STORE_VERSION = "2";
2592
2592
  function stateDir() {
2593
2593
  const home = process.env.HOME || process.env.USERPROFILE || "";
@@ -2828,10 +2828,10 @@ function activeProcedureAnnounces(store) {
2828
2828
  return lines;
2829
2829
  }
2830
2830
 
2831
- // src/procedure-runtime.ts
2831
+ // ../greprag/packages/cli/src/procedure-runtime.ts
2832
2832
  var crypto4 = __toESM(require("crypto"));
2833
2833
 
2834
- // src/project-anchor.ts
2834
+ // ../greprag/packages/cli/src/project-anchor.ts
2835
2835
  var path3 = __toESM(require("path"));
2836
2836
  var fs3 = __toESM(require("fs"));
2837
2837
  var crypto2 = __toESM(require("crypto"));
@@ -3013,7 +3013,7 @@ function readAnchor2(cwd) {
3013
3013
  };
3014
3014
  }
3015
3015
 
3016
- // src/procedure-watch.ts
3016
+ // ../greprag/packages/cli/src/procedure-watch.ts
3017
3017
  var path4 = __toESM(require("path"));
3018
3018
  var fs4 = __toESM(require("fs"));
3019
3019
  var TIER1_LEARN_TRIGGERS = [
@@ -3074,7 +3074,7 @@ function openWatchIfIdle(projectId, verb, phase, shadowRunId) {
3074
3074
  return true;
3075
3075
  }
3076
3076
 
3077
- // src/procedure-shadow.ts
3077
+ // ../greprag/packages/cli/src/procedure-shadow.ts
3078
3078
  var crypto3 = __toESM(require("crypto"));
3079
3079
  var fs5 = __toESM(require("fs"));
3080
3080
  var path5 = __toESM(require("path"));
@@ -3140,7 +3140,7 @@ function recordMissingLifecycleShadowTrigger(params) {
3140
3140
  });
3141
3141
  }
3142
3142
 
3143
- // src/procedure-runtime.ts
3143
+ // ../greprag/packages/cli/src/procedure-runtime.ts
3144
3144
  function extractLatestOpenCodeProcedurePrompt(messages) {
3145
3145
  let userCount = 0;
3146
3146
  let latest = null;
@@ -3226,7 +3226,7 @@ function runProcedurePromptCheck(params) {
3226
3226
  }
3227
3227
  }
3228
3228
 
3229
- // src/archive/glyph-font.ts
3229
+ // ../greprag/packages/cli/src/archive/glyph-font.ts
3230
3230
  var GLYPH_W = 8;
3231
3231
  var GLYPH_H = 8;
3232
3232
  var FIRST_CODE = 32;
@@ -3350,7 +3350,7 @@ function signatureForRows(bytes) {
3350
3350
  return bytes.map((b) => (b & 255).toString(16).padStart(2, "0")).join("");
3351
3351
  }
3352
3352
 
3353
- // src/archive/png-codec.ts
3353
+ // ../greprag/packages/cli/src/archive/png-codec.ts
3354
3354
  var zlib = __toESM(require("zlib"));
3355
3355
  var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
3356
3356
  var crcTable = null;
@@ -3492,7 +3492,7 @@ function decodePng(buf) {
3492
3492
  return { width, height, pixels };
3493
3493
  }
3494
3494
 
3495
- // src/archive/page-renderer.ts
3495
+ // ../greprag/packages/cli/src/archive/page-renderer.ts
3496
3496
  var PAGE_SIZE = 1568;
3497
3497
  var CELL_W = GLYPH_W;
3498
3498
  var CELL_H = 10;
@@ -3618,7 +3618,7 @@ function verifyRoundTrip(page) {
3618
3618
  return `length mismatch: rendered ${a.length} decoded ${b.length}`;
3619
3619
  }
3620
3620
 
3621
- // src/opencode-plugin-png.ts
3621
+ // ../greprag/packages/cli/src/opencode-plugin-png.ts
3622
3622
  var CCR_MARKER_HASH_LEN2 = 16;
3623
3623
  function ccrPngMarker(hash) {
3624
3624
  return `<<ccr:${hash.slice(0, CCR_MARKER_HASH_LEN2)}>>`;
@@ -3808,7 +3808,7 @@ function recodeMessagesToPng(messages, opts) {
3808
3808
  return stats;
3809
3809
  }
3810
3810
 
3811
- // src/skill-activation-manifest.ts
3811
+ // ../greprag/packages/cli/src/skill-activation-manifest.ts
3812
3812
  var fs6 = __toESM(require("fs"));
3813
3813
  var path6 = __toESM(require("path"));
3814
3814
  var MAX_NATIVE_SKILL_FILES = 1500;
@@ -3909,7 +3909,7 @@ function activationFromApiRows(params) {
3909
3909
  return buildMirroredSkillActivation(entries, readNativeSkillNames(params));
3910
3910
  }
3911
3911
 
3912
- // src/opencode-plugin.ts
3912
+ // ../greprag/packages/cli/src/opencode-plugin.ts
3913
3913
  var DEBUG_LOG_PATH = path7.join(os2.homedir(), ".greprag", "opencode-plugin-debug.log");
3914
3914
  var _debugLogReady = false;
3915
3915
  function dlogInit() {
@@ -4490,6 +4490,7 @@ function grepragBinCached() {
4490
4490
  return _cachedGrepragBin;
4491
4491
  }
4492
4492
  var _crushShapeLogged = false;
4493
+ var _crushSeenHashes;
4493
4494
  function inlineCrushEnabled() {
4494
4495
  const v = process.env.GREPRAG_OPENCODE_CRUSH;
4495
4496
  return v === "true" || v === "1";
@@ -4732,7 +4733,8 @@ ALWAYS pass \`--from-session ${sid82}\` when you run \`greprag send\`, so recipi
4732
4733
  const thresholdEnv = parseInt(process.env.GREPRAG_OPENCODE_CRUSH_MIN_BYTES || "", 10);
4733
4734
  const stats = crushMessages(msgs, {
4734
4735
  stash: makeCcrStash(),
4735
- 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())
4736
4738
  });
4737
4739
  if (stats.partsCrushed > 0 || stats.partsReverted > 0) {
4738
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.17",
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": {