@tznthou/ccrecall 0.7.2 → 0.8.0

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.
@@ -168,7 +168,14 @@ ccrecall-extract() {
168
168
  if [[ -f "$prompt_file" ]]; then
169
169
  prompt=$(cat "$prompt_file")
170
170
  else
171
- prompt="You are a memory extraction agent. Save 0-5 lasting insights via recall_save. Each memory must be self-contained with a key slug for dedup. Set projectId to \"${project_id}\" for project-specific knowledge; omit for cross-project knowledge."
171
+ # Keep the "lead with the prescription" rule in sync with
172
+ # extraction-prompt.md — only the first 149 chars of a memory survive
173
+ # injection, so a memory that opens with narrative arrives unactionable.
174
+ # tests/extraction-prompt-prescription.test.ts pins the rule AND the number
175
+ # below against token-budget.ts, in both copies. 🔴 Until 2026-09-08 this
176
+ # comment already said that while no test read this line's number at all:
177
+ # it could say 999 and stay green. Verified by mutation, both directions.
178
+ prompt="You are a memory extraction agent. Save 0-5 lasting insights via recall_save. Each memory must be self-contained with a key slug for dedup. Only the first 149 characters survive injection into a future session, so lead with what a future reader should DO and put the evidence, file names and war story after it. Set projectId to \"${project_id}\" for project-specific knowledge; omit for cross-project knowledge. Always pass origin=\"agent-inferred\": you are reading a finished transcript with nobody watching, and that flag is what stops your save from overwriting a memory the user wrote by hand under the same key. Pass messageId: the uuid in the \"--- human [<uuid>] ---\" header of the one message a memory came from, copied verbatim — omit it when no single message is the source, and never guess a uuid."
172
179
  fi
173
180
 
174
181
  # Append runtime context (projectId) to the prompt
@@ -196,21 +203,46 @@ ccrecall-extract() {
196
203
  local session_transcript
197
204
  # A2: -R + fromjson? skips malformed JSONL lines instead of aborting
198
205
  # A1+A5: head -c 200000 + iconv -c strips incomplete UTF-8 at boundary
206
+ #
207
+ # Q1 (2026-09-11): each header carries the message uuid, so a memory can
208
+ # name the message it came from (`memories.message_id`, 0/1639 filled
209
+ # before this). The transcript was the blocker, not the schema: the
210
+ # column and the recall_save parameter both already existed, and the
211
+ # model had nothing to point at.
212
+ #
213
+ # Costs ~40 bytes per message against the 200KB cap. Measured on a real
214
+ # 151-message session: 119,507 -> 125,396 bytes, +4.93%. (The share is
215
+ # higher on an English-heavy session — that one is mostly CJK, where the
216
+ # body text costs 3 bytes a character and dilutes the ASCII uuids. head
217
+ # -c counts bytes, not characters.) A session already at the cap loses
218
+ # that much off its tail, which is a real cost, accepted because the tail
219
+ # is what the model has most recently read in full, while provenance is
220
+ # unrecoverable after the fact — 65.6% of stored memories have already
221
+ # been rewritten by compression.
222
+ #
223
+ # gsub keeps the header structurally intact: the transcript is untrusted
224
+ # data, and a uuid holding `] ---` would otherwise let a crafted JSONL
225
+ # forge message boundaries. Restricting the characters rather than
226
+ # matching a uuid shape is deliberate — a format gate would stop printing
227
+ # citations the day Claude Code changes its id format, silently. A value
228
+ # mangled by gsub simply fails verification on the write side.
199
229
  session_transcript=$(jq -R -r '
200
230
  fromjson? // empty |
231
+ (((.uuid | strings) // "") | gsub("[^A-Za-z0-9-]"; "")) as $u |
232
+ (if $u == "" then "" else " [" + $u + "]" end) as $cite |
201
233
  if .type == "user" and .message then
202
234
  .message.content |
203
235
  if type == "array" then
204
236
  [.[] | select(.type == "text") | .text // empty] | join("\n") |
205
- if . != "" then "--- human ---\n" + . else empty end
237
+ if . != "" then "--- human" + $cite + " ---\n" + . else empty end
206
238
  elif type == "string" then
207
- if . != "" then "--- human ---\n" + . else empty end
239
+ if . != "" then "--- human" + $cite + " ---\n" + . else empty end
208
240
  else empty end
209
241
  elif .type == "assistant" and .message then
210
242
  .message.content |
211
243
  if type == "array" then
212
244
  [.[] | select(.type == "text") | .text // empty] | join("\n") |
213
- if . != "" then "--- assistant ---\n" + . else empty end
245
+ if . != "" then "--- assistant" + $cite + " ---\n" + . else empty end
214
246
  else empty end
215
247
  else empty end
216
248
  ' "$jsonl_path" 2>/dev/null | head -c 200000 | iconv -c -f utf-8 -t utf-8)