clawmem 0.2.6 → 0.2.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmem",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "On-device context engine and memory for AI agents. Claude Code and OpenClaw. Hooks + MCP server + hybrid RAG search.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/hooks.ts CHANGED
@@ -243,7 +243,46 @@ export function readTranscript(
243
243
  roleFilter?: "user" | "assistant"
244
244
  ): TranscriptMessage[] {
245
245
  try {
246
- const content = require("fs").readFileSync(transcriptPath, "utf-8");
246
+ const fs = require("fs");
247
+ const stat = fs.statSync(transcriptPath);
248
+ let content: string;
249
+
250
+ // For large transcripts (>10MB), read backwards in chunks until we have enough lines
251
+ if (stat.size > 10 * 1024 * 1024) {
252
+ const chunkSize = 2 * 1024 * 1024; // 2MB chunks
253
+ const maxChunks = 5; // Up to 10MB of tail
254
+ const targetLines = lastN * 3; // Overshoot — not all lines are role messages
255
+ const buffers: Buffer[] = [];
256
+ let totalRead = 0;
257
+
258
+ // Accumulate raw Buffers (decode once after assembly to avoid UTF-8 boundary corruption)
259
+ const fd = fs.openSync(transcriptPath, "r");
260
+ try {
261
+ for (let chunk = 0; chunk < maxChunks; chunk++) {
262
+ const readSize = Math.min(chunkSize, stat.size - totalRead);
263
+ if (readSize <= 0) break;
264
+ const offset = Math.max(0, stat.size - totalRead - readSize);
265
+ const buf = Buffer.alloc(readSize);
266
+ fs.readSync(fd, buf, 0, readSize, offset);
267
+ buffers.unshift(buf);
268
+ totalRead += readSize;
269
+
270
+ // Check line count on decoded text to see if we have enough
271
+ const decoded = Buffer.concat(buffers).toString("utf-8");
272
+ if (decoded.split("\n").length >= targetLines) break;
273
+ }
274
+ } finally {
275
+ fs.closeSync(fd);
276
+ }
277
+
278
+ const assembled = Buffer.concat(buffers).toString("utf-8");
279
+ // Drop first partial line (we likely started mid-line)
280
+ const firstNewline = assembled.indexOf("\n");
281
+ content = firstNewline > 0 ? assembled.slice(firstNewline + 1) : assembled;
282
+ } else {
283
+ content = fs.readFileSync(transcriptPath, "utf-8");
284
+ }
285
+
247
286
  const lines = content.split("\n").filter((l: string) => l.trim());
248
287
  const messages: TranscriptMessage[] = [];
249
288
 
@@ -284,7 +323,7 @@ export function readTranscript(
284
323
  }
285
324
 
286
325
  /**
287
- * Validate a transcript path (security: must be absolute, .jsonl, regular file, <50MB).
326
+ * Validate a transcript path (security: must be absolute, .jsonl, regular file, <1GB).
288
327
  */
289
328
  export function validateTranscriptPath(path: string | undefined): string | null {
290
329
  if (!path) return null;
@@ -294,7 +333,7 @@ export function validateTranscriptPath(path: string | undefined): string | null
294
333
  try {
295
334
  const stat = require("fs").statSync(path);
296
335
  if (!stat.isFile()) return null;
297
- if (stat.size > 50 * 1024 * 1024) return null; // 50MB limit
336
+ if (stat.size > 1024 * 1024 * 1024) return null; // 1GB sanity limit (readTranscript tail-reads large files)
298
337
  return path;
299
338
  } catch {
300
339
  return null;
package/src/mcp.ts CHANGED
@@ -878,11 +878,12 @@ This is the recommended entry point for ALL memory queries.`,
878
878
  if (tokens.length > 0) {
879
879
  const minMatch = Math.max(2, Math.ceil(tokens.length / 2));
880
880
  const titleHits = store.db.prepare(`
881
- SELECT collection || '/' || path as displayPath, title,
882
- ${tokens.map((_, i) => `(CASE WHEN LOWER(title) LIKE ? THEN 1 ELSE 0 END)`).join(" + ")} as match_count
883
- FROM documents
884
- WHERE active = 1 AND invalidated_at IS NULL
885
- HAVING match_count >= ?
881
+ SELECT displayPath, title, match_count FROM (
882
+ SELECT collection || '/' || path as displayPath, title, modified_at,
883
+ ${tokens.map(() => `(CASE WHEN LOWER(title) LIKE ? THEN 1 ELSE 0 END)`).join(" + ")} as match_count
884
+ FROM documents
885
+ WHERE active = 1 AND invalidated_at IS NULL
886
+ ) WHERE match_count >= ?
886
887
  ORDER BY match_count DESC, modified_at DESC
887
888
  LIMIT ?
888
889
  `).all(...tokens.map(t => `%${t}%`), minMatch, limit) as { displayPath: string; title: string; match_count: number }[];