clawvault 1.9.2 → 1.9.3

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.
@@ -58,33 +58,6 @@ function estimateTokens(text) {
58
58
  }
59
59
  return Math.ceil(text.length / 4);
60
60
  }
61
- function fitWithinBudget(items, budget) {
62
- if (!Number.isFinite(budget) || budget <= 0) {
63
- return [];
64
- }
65
- const sorted = items.map((item, index) => ({ ...item, index })).sort((a, b) => {
66
- if (a.priority !== b.priority) {
67
- return a.priority - b.priority;
68
- }
69
- return a.index - b.index;
70
- });
71
- let remaining = Math.floor(budget);
72
- const fitted = [];
73
- for (const item of sorted) {
74
- if (!item.text.trim()) {
75
- continue;
76
- }
77
- const cost = estimateTokens(item.text);
78
- if (cost <= remaining) {
79
- fitted.push({ text: item.text, source: item.source });
80
- remaining -= cost;
81
- }
82
- if (remaining <= 0) {
83
- break;
84
- }
85
- }
86
- return fitted;
87
- }
88
61
 
89
62
  // src/commands/context.ts
90
63
  var DEFAULT_LIMIT = 5;
@@ -223,12 +196,8 @@ async function buildDailyContextItems(vault) {
223
196
  }
224
197
  const relativePath = path2.relative(vault.getPath(), document.path).split(path2.sep).join("/");
225
198
  const snippet = estimateSnippet(document.content);
226
- const sourceId = `daily:${date}:${relativePath}`;
227
199
  items.push({
228
200
  priority: 2,
229
- source: sourceId,
230
- text: `${date}
231
- ${snippet}`,
232
201
  entry: {
233
202
  title: `Daily note ${date}`,
234
203
  path: relativePath,
@@ -247,17 +216,13 @@ function buildObservationContextItems(vaultPath) {
247
216
  const observationMarkdown = readObservations(vaultPath, OBSERVATION_LOOKBACK_DAYS);
248
217
  const parsed = parseObservationLines(observationMarkdown);
249
218
  const items = [];
250
- for (const [index, observation] of parsed.entries()) {
219
+ for (const observation of parsed) {
251
220
  const priority = observationPriorityToRank(observation.priority);
252
221
  const modifiedDate = asDate(observation.date, /* @__PURE__ */ new Date());
253
222
  const date = observation.date || modifiedDate.toISOString().slice(0, 10);
254
223
  const snippet = estimateSnippet(observation.content);
255
- const sourceId = `observation:${priority}:${date}:${index}`;
256
224
  items.push({
257
225
  priority,
258
- source: sourceId,
259
- text: `${observation.priority} ${date}
260
- ${snippet}`,
261
226
  entry: {
262
227
  title: `${observation.priority} observation (${date})`,
263
228
  path: `observations/${date}.md`,
@@ -273,7 +238,7 @@ ${snippet}`,
273
238
  return items;
274
239
  }
275
240
  function buildSearchContextItems(vault, results) {
276
- return results.map((result, index) => {
241
+ return results.map((result) => {
277
242
  const relativePath = path2.relative(vault.getPath(), result.document.path).split(path2.sep).join("/");
278
243
  const entry = {
279
244
  title: result.document.title,
@@ -287,34 +252,53 @@ function buildSearchContextItems(vault, results) {
287
252
  };
288
253
  return {
289
254
  priority: 3,
290
- source: `search:${index}:${entry.path}`,
291
- text: `${entry.title}
292
- ${entry.snippet}`,
293
255
  entry
294
256
  };
295
257
  });
296
258
  }
297
- function applyTokenBudget(items, budget) {
259
+ function renderEntryBlock(entry) {
260
+ return `### ${entry.title} (${entry.source}, score: ${entry.score.toFixed(2)}, ${entry.age})
261
+ ${entry.snippet}
262
+
263
+ `;
264
+ }
265
+ function truncateToBudget(text, budget) {
266
+ if (!Number.isFinite(budget) || budget <= 0) {
267
+ return "";
268
+ }
269
+ const maxChars = Math.max(0, Math.floor(budget) * 4);
270
+ if (text.length <= maxChars) {
271
+ return text;
272
+ }
273
+ return text.slice(0, maxChars).trimEnd();
274
+ }
275
+ function applyTokenBudget(items, task, budget) {
276
+ const fullContext = items.map((item) => item.entry);
277
+ const fullMarkdown = formatContextMarkdown(task, fullContext);
298
278
  if (budget === void 0) {
299
- return items.map((item) => item.entry);
279
+ return { context: fullContext, markdown: fullMarkdown };
300
280
  }
301
- const selected = fitWithinBudget(
302
- items.map((item) => ({
303
- text: item.text,
304
- priority: item.priority,
305
- source: item.source
306
- })),
307
- budget
308
- );
309
- const bySource = new Map(items.map((item) => [item.source, item.entry]));
281
+ const normalizedBudget = Math.max(1, Math.floor(budget));
282
+ const header = `## Relevant Context for: ${task}
283
+
284
+ `;
285
+ let remaining = normalizedBudget - estimateTokens(header);
310
286
  const selectedEntries = [];
311
- for (const selectedItem of selected) {
312
- const entry = bySource.get(selectedItem.source);
313
- if (entry) {
314
- selectedEntries.push(entry);
287
+ for (const item of [...items].sort((a, b) => a.priority - b.priority)) {
288
+ if (remaining <= 0) {
289
+ break;
290
+ }
291
+ const cost = estimateTokens(renderEntryBlock(item.entry));
292
+ if (cost <= remaining) {
293
+ selectedEntries.push(item.entry);
294
+ remaining -= cost;
315
295
  }
316
296
  }
317
- return selectedEntries;
297
+ const markdown = truncateToBudget(formatContextMarkdown(task, selectedEntries), normalizedBudget);
298
+ return {
299
+ context: selectedEntries,
300
+ markdown
301
+ };
318
302
  }
319
303
  async function buildContext(task, options) {
320
304
  const normalizedTask = task.trim();
@@ -343,12 +327,12 @@ async function buildContext(task, options) {
343
327
  ...yellowObservations,
344
328
  ...greenObservations
345
329
  ];
346
- const context = applyTokenBudget(ordered, options.budget);
330
+ const { context, markdown } = applyTokenBudget(ordered, normalizedTask, options.budget);
347
331
  return {
348
332
  task: normalizedTask,
349
333
  generated: (/* @__PURE__ */ new Date()).toISOString(),
350
334
  context,
351
- markdown: formatContextMarkdown(normalizedTask, context)
335
+ markdown
352
336
  };
353
337
  }
354
338
  async function contextCommand(task, options) {
@@ -1,7 +1,7 @@
1
1
  // src/observer/compressor.ts
2
2
  var DATE_HEADING_RE = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
3
3
  var OBSERVATION_LINE_RE = /^(🔴|🟡|🟢)\s+(.+)$/u;
4
- var CRITICAL_RE = /\b(decid(?:e|ed|ing|ion)|error|fail(?:ed|ure)?|prefer(?:ence)?|block(?:ed|er)?|must|required?|urgent)\b/i;
4
+ var CRITICAL_RE = /(?:\b(?:decision|decided|chose|selected)\s*:|\bdecid(?:e|ed|ing|ion)\b|\berror\b|\bfail(?:ed|ure)?\b|\bprefer(?:ence)?\b|\bblock(?:ed|er)?\b|\bmust\b|\brequired?\b|\burgent\b)/i;
5
5
  var NOTABLE_RE = /\b(context|pattern|architecture|approach|trade[- ]?off|milestone|notable)\b/i;
6
6
  var Compressor = class {
7
7
  model;
@@ -191,13 +191,40 @@ ${cleaned}`;
191
191
  if (existingSections.size === 0) {
192
192
  return this.renderSections(incomingSections);
193
193
  }
194
+ for (const [date, lines] of existingSections.entries()) {
195
+ existingSections.set(date, this.deduplicateObservationLines(lines));
196
+ }
194
197
  for (const [date, lines] of incomingSections.entries()) {
195
- const current = existingSections.get(date) ?? [];
196
- current.push(...lines);
198
+ const current = this.deduplicateObservationLines(existingSections.get(date) ?? []);
199
+ const seen = new Set(current.map((line) => this.normalizeObservationContent(line.content)));
200
+ for (const line of lines) {
201
+ const normalized = this.normalizeObservationContent(line.content);
202
+ if (!normalized || seen.has(normalized)) {
203
+ continue;
204
+ }
205
+ seen.add(normalized);
206
+ current.push(line);
207
+ }
197
208
  existingSections.set(date, current);
198
209
  }
199
210
  return this.renderSections(existingSections);
200
211
  }
212
+ deduplicateObservationLines(lines) {
213
+ const deduped = [];
214
+ const seen = /* @__PURE__ */ new Set();
215
+ for (const line of lines) {
216
+ const normalized = this.normalizeObservationContent(line.content);
217
+ if (!normalized || seen.has(normalized)) {
218
+ continue;
219
+ }
220
+ seen.add(normalized);
221
+ deduped.push(line);
222
+ }
223
+ return deduped;
224
+ }
225
+ normalizeObservationContent(content) {
226
+ return content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\s+/g, " ").trim().toLowerCase();
227
+ }
201
228
  parseSections(markdown) {
202
229
  const sections = /* @__PURE__ */ new Map();
203
230
  let currentDate = null;
@@ -398,7 +425,10 @@ var CATEGORY_PATTERNS = [
398
425
  category: "people",
399
426
  patterns: [
400
427
  /\b(said|asked|told|mentioned|emailed|called|messaged|met with)\b/i,
401
- /\b(client|partner|team|colleague|contact)\b/i
428
+ /\b(client|partner|team|colleague|contact)\b/i,
429
+ /\b(?:Pedro|Justin|Maria|Sarah|[A-Z][a-z]+ (?:said|asked|told|mentioned))\b/,
430
+ /\b(?:talked to|met with)\s+[A-Z][a-z]+\b/i,
431
+ /\b[A-Z][a-z]+\s+from\b/
402
432
  ]
403
433
  },
404
434
  {
@@ -503,6 +533,8 @@ ${entry}
503
533
  };
504
534
 
505
535
  // src/observer/observer.ts
536
+ var DATE_HEADING_RE4 = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
537
+ var OBSERVATION_LINE_RE4 = /^(🔴|🟡|🟢)\s+(.+)$/u;
506
538
  var Observer = class {
507
539
  vaultPath;
508
540
  observationsDir;
@@ -538,9 +570,14 @@ var Observer = class {
538
570
  return;
539
571
  }
540
572
  const todayPath = this.getObservationPath(this.now());
541
- const existing = this.readObservationFile(todayPath);
542
- const compressed = (await this.compressor.compress(this.pendingMessages, existing)).trim();
573
+ const existingRaw = this.readObservationFile(todayPath);
574
+ const existing = this.deduplicateObservationMarkdown(existingRaw);
575
+ if (existingRaw.trim() !== existing) {
576
+ this.writeObservationFile(todayPath, existing);
577
+ }
578
+ const compressedRaw = (await this.compressor.compress(this.pendingMessages, existing)).trim();
543
579
  this.pendingMessages = [];
580
+ const compressed = this.deduplicateObservationMarkdown(compressedRaw);
544
581
  if (!compressed) {
545
582
  return;
546
583
  }
@@ -561,9 +598,14 @@ var Observer = class {
561
598
  return { observations: this.observationsCache, routingSummary: this.lastRoutingSummary };
562
599
  }
563
600
  const todayPath = this.getObservationPath(this.now());
564
- const existing = this.readObservationFile(todayPath);
565
- const compressed = (await this.compressor.compress(this.pendingMessages, existing)).trim();
601
+ const existingRaw = this.readObservationFile(todayPath);
602
+ const existing = this.deduplicateObservationMarkdown(existingRaw);
603
+ if (existingRaw.trim() !== existing) {
604
+ this.writeObservationFile(todayPath, existing);
605
+ }
606
+ const compressedRaw = (await this.compressor.compress(this.pendingMessages, existing)).trim();
566
607
  this.pendingMessages = [];
608
+ const compressed = this.deduplicateObservationMarkdown(compressedRaw);
567
609
  if (compressed) {
568
610
  this.writeObservationFile(todayPath, compressed);
569
611
  this.observationsCache = compressed;
@@ -612,6 +654,74 @@ var Observer = class {
612
654
  }
613
655
  return files.map((filePath) => this.readObservationFile(filePath)).filter(Boolean).join("\n\n");
614
656
  }
657
+ deduplicateObservationMarkdown(markdown) {
658
+ const parsed = this.parseSections(markdown);
659
+ if (parsed.size === 0) {
660
+ return markdown.trim();
661
+ }
662
+ for (const [date, lines] of parsed.entries()) {
663
+ const seen = /* @__PURE__ */ new Set();
664
+ const deduped = [];
665
+ for (const line of lines) {
666
+ const normalized = this.normalizeObservationContent(line.content);
667
+ if (!normalized || seen.has(normalized)) {
668
+ continue;
669
+ }
670
+ seen.add(normalized);
671
+ deduped.push(line);
672
+ }
673
+ parsed.set(date, deduped);
674
+ }
675
+ return this.renderSections(parsed);
676
+ }
677
+ parseSections(markdown) {
678
+ const sections = /* @__PURE__ */ new Map();
679
+ let currentDate = null;
680
+ for (const rawLine of markdown.split(/\r?\n/)) {
681
+ const dateMatch = rawLine.match(DATE_HEADING_RE4);
682
+ if (dateMatch) {
683
+ currentDate = dateMatch[1];
684
+ if (!sections.has(currentDate)) {
685
+ sections.set(currentDate, []);
686
+ }
687
+ continue;
688
+ }
689
+ if (!currentDate) {
690
+ continue;
691
+ }
692
+ const lineMatch = rawLine.match(OBSERVATION_LINE_RE4);
693
+ if (!lineMatch) {
694
+ continue;
695
+ }
696
+ const current = sections.get(currentDate) ?? [];
697
+ current.push({
698
+ priority: lineMatch[1],
699
+ content: lineMatch[2].trim()
700
+ });
701
+ sections.set(currentDate, current);
702
+ }
703
+ return sections;
704
+ }
705
+ renderSections(sections) {
706
+ const chunks = [];
707
+ const dates = [...sections.keys()].sort((a, b) => a.localeCompare(b));
708
+ for (const date of dates) {
709
+ const lines = sections.get(date) ?? [];
710
+ if (lines.length === 0) {
711
+ continue;
712
+ }
713
+ chunks.push(`## ${date}`);
714
+ chunks.push("");
715
+ for (const line of lines) {
716
+ chunks.push(`${line.priority} ${line.content}`);
717
+ }
718
+ chunks.push("");
719
+ }
720
+ return chunks.join("\n").trim();
721
+ }
722
+ normalizeObservationContent(content) {
723
+ return content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\s+/g, " ").trim().toLowerCase();
724
+ }
615
725
  async reflectIfNeeded() {
616
726
  const corpus = this.readObservationCorpus();
617
727
  if (this.estimateTokens(corpus) < this.reflectThreshold) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Observer,
3
3
  parseSessionFile
4
- } from "./chunk-PBLNXOPM.js";
4
+ } from "./chunk-EQ2AZVBX.js";
5
5
 
6
6
  // src/commands/observe.ts
7
7
  import * as fs2 from "fs";
@@ -16,13 +16,17 @@ var SessionWatcher = class {
16
16
  watchPath;
17
17
  observer;
18
18
  ignoreInitial;
19
+ debounceMs;
19
20
  watcher = null;
20
21
  fileOffsets = /* @__PURE__ */ new Map();
22
+ pendingPaths = /* @__PURE__ */ new Set();
23
+ debounceTimer = null;
21
24
  processingQueue = Promise.resolve();
22
25
  constructor(watchPath, observer, options = {}) {
23
26
  this.watchPath = path.resolve(watchPath);
24
27
  this.observer = observer;
25
28
  this.ignoreInitial = options.ignoreInitial ?? false;
29
+ this.debounceMs = options.debounceMs ?? 500;
26
30
  }
27
31
  async start() {
28
32
  if (!fs.existsSync(this.watchPath)) {
@@ -37,18 +41,44 @@ var SessionWatcher = class {
37
41
  }
38
42
  });
39
43
  const enqueue = (changedPath) => {
40
- this.processingQueue = this.processingQueue.then(() => this.consumeFile(changedPath)).catch(() => void 0);
44
+ this.pendingPaths.add(path.resolve(changedPath));
45
+ this.scheduleDrain();
41
46
  };
42
47
  this.watcher.on("add", enqueue);
43
48
  this.watcher.on("change", enqueue);
44
49
  this.watcher.on("unlink", (deletedPath) => {
45
- this.fileOffsets.delete(path.resolve(deletedPath));
50
+ const resolved = path.resolve(deletedPath);
51
+ this.fileOffsets.delete(resolved);
52
+ this.pendingPaths.delete(resolved);
53
+ });
54
+ await new Promise((resolve3, reject) => {
55
+ this.watcher?.once("ready", () => resolve3());
56
+ this.watcher?.once("error", (error) => reject(error));
46
57
  });
47
58
  }
48
59
  async stop() {
60
+ if (this.debounceTimer) {
61
+ clearTimeout(this.debounceTimer);
62
+ this.debounceTimer = null;
63
+ }
64
+ this.pendingPaths.clear();
65
+ await this.processingQueue.catch(() => void 0);
49
66
  await this.watcher?.close();
50
67
  this.watcher = null;
51
68
  }
69
+ scheduleDrain() {
70
+ if (this.debounceTimer) {
71
+ clearTimeout(this.debounceTimer);
72
+ }
73
+ this.debounceTimer = setTimeout(() => {
74
+ this.debounceTimer = null;
75
+ const nextPaths = [...this.pendingPaths];
76
+ this.pendingPaths.clear();
77
+ for (const changedPath of nextPaths) {
78
+ this.processingQueue = this.processingQueue.then(() => this.consumeFile(changedPath)).catch(() => void 0);
79
+ }
80
+ }, this.debounceMs);
81
+ }
52
82
  async consumeFile(filePath) {
53
83
  const resolved = path.resolve(filePath);
54
84
  if (!fs.existsSync(resolved)) {
@@ -3,7 +3,7 @@ import {
3
3
  contextCommand,
4
4
  formatContextMarkdown,
5
5
  registerContextCommand
6
- } from "../chunk-S3TOK4VI.js";
6
+ } from "../chunk-AVPHNEDB.js";
7
7
  import "../chunk-3HFB7EMU.js";
8
8
  import "../chunk-MIIXBNO3.js";
9
9
  export {
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  observeCommand,
3
3
  registerObserveCommand
4
- } from "../chunk-P3KFRS2W.js";
5
- import "../chunk-PBLNXOPM.js";
4
+ } from "../chunk-JATU7JVY.js";
5
+ import "../chunk-EQ2AZVBX.js";
6
6
  export {
7
7
  observeCommand,
8
8
  registerObserveCommand
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  Observer,
15
15
  parseSessionFile
16
- } from "../chunk-PBLNXOPM.js";
16
+ } from "../chunk-EQ2AZVBX.js";
17
17
 
18
18
  // src/commands/sleep.ts
19
19
  import * as fs from "fs";
package/dist/index.d.ts CHANGED
@@ -307,6 +307,10 @@ declare class Observer {
307
307
  private writeObservationFile;
308
308
  private getObservationFiles;
309
309
  private readObservationCorpus;
310
+ private deduplicateObservationMarkdown;
311
+ private parseSections;
312
+ private renderSections;
313
+ private normalizeObservationContent;
310
314
  private reflectIfNeeded;
311
315
  }
312
316
 
@@ -329,6 +333,8 @@ declare class Compressor {
329
333
  private normalizeLlmOutput;
330
334
  private fallbackCompression;
331
335
  private mergeObservations;
336
+ private deduplicateObservationLines;
337
+ private normalizeObservationContent;
332
338
  private parseSections;
333
339
  private renderSections;
334
340
  private inferPriority;
@@ -356,17 +362,22 @@ declare class Reflector {
356
362
 
357
363
  interface SessionWatcherOptions {
358
364
  ignoreInitial?: boolean;
365
+ debounceMs?: number;
359
366
  }
360
367
  declare class SessionWatcher {
361
368
  private readonly watchPath;
362
369
  private readonly observer;
363
370
  private readonly ignoreInitial;
371
+ private readonly debounceMs;
364
372
  private watcher;
365
373
  private fileOffsets;
374
+ private pendingPaths;
375
+ private debounceTimer;
366
376
  private processingQueue;
367
377
  constructor(watchPath: string, observer: Observer, options?: SessionWatcherOptions);
368
378
  start(): Promise<void>;
369
379
  stop(): Promise<void>;
380
+ private scheduleDrain;
370
381
  private consumeFile;
371
382
  }
372
383
 
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  contextCommand,
17
17
  formatContextMarkdown,
18
18
  registerContextCommand
19
- } from "./chunk-S3TOK4VI.js";
19
+ } from "./chunk-AVPHNEDB.js";
20
20
  import {
21
21
  ClawVault,
22
22
  createVault,
@@ -41,13 +41,13 @@ import {
41
41
  SessionWatcher,
42
42
  observeCommand,
43
43
  registerObserveCommand
44
- } from "./chunk-P3KFRS2W.js";
44
+ } from "./chunk-JATU7JVY.js";
45
45
  import {
46
46
  Compressor,
47
47
  Observer,
48
48
  Reflector,
49
49
  parseSessionFile
50
- } from "./chunk-PBLNXOPM.js";
50
+ } from "./chunk-EQ2AZVBX.js";
51
51
 
52
52
  // src/index.ts
53
53
  import * as fs from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawvault",
3
- "version": "1.9.2",
3
+ "version": "1.9.3",
4
4
  "description": "ClawVault™ - 🐘 An elephant never forgets. Structured memory for OpenClaw agents. Context death resilience, Obsidian-compatible markdown, local semantic search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",