clawvault 2.1.2 → 2.2.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.
Files changed (54) hide show
  1. package/bin/command-registration.test.js +6 -1
  2. package/bin/help-contract.test.js +2 -0
  3. package/bin/register-maintenance-commands.js +111 -0
  4. package/bin/register-query-commands.js +32 -1
  5. package/bin/register-session-lifecycle-commands.js +2 -0
  6. package/dist/{chunk-5MQB7B37.js → chunk-2HM7ZI4X.js} +268 -434
  7. package/dist/{chunk-TBVI4N53.js → chunk-6RIHODNR.js} +120 -95
  8. package/dist/chunk-73P7XCQM.js +104 -0
  9. package/dist/{chunk-MIIXBNO3.js → chunk-DHBDH4DN.js} +4 -0
  10. package/dist/chunk-GJEGPO7U.js +49 -0
  11. package/dist/chunk-GQVYQCY5.js +396 -0
  12. package/dist/{chunk-TXO34J3O.js → chunk-H7JW4L7H.js} +1 -1
  13. package/dist/{chunk-QFBKWDYR.js → chunk-IFGDPIFI.js} +3 -3
  14. package/dist/chunk-K6XHCUFL.js +123 -0
  15. package/dist/{chunk-PIJGYMQZ.js → chunk-KNDVXXKC.js} +1 -1
  16. package/dist/chunk-L6NB43WV.js +472 -0
  17. package/dist/{chunk-FEQ2CQ3Y.js → chunk-LB6P4CD5.js} +20 -7
  18. package/dist/chunk-MGDEINGP.js +99 -0
  19. package/dist/chunk-MQUJNOHK.js +58 -0
  20. package/dist/chunk-P5EPF6MB.js +182 -0
  21. package/dist/chunk-VR5NE7PZ.js +45 -0
  22. package/dist/chunk-WZI3OAE5.js +111 -0
  23. package/dist/chunk-Z2XBWN7A.js +247 -0
  24. package/dist/{chunk-O5V7SD5C.js → chunk-ZZA73MFY.js} +1 -1
  25. package/dist/commands/archive.d.ts +11 -0
  26. package/dist/commands/archive.js +11 -0
  27. package/dist/commands/context.d.ts +1 -1
  28. package/dist/commands/context.js +6 -4
  29. package/dist/commands/doctor.js +6 -6
  30. package/dist/commands/graph.js +2 -2
  31. package/dist/commands/link.js +1 -1
  32. package/dist/commands/migrate-observations.d.ts +19 -0
  33. package/dist/commands/migrate-observations.js +13 -0
  34. package/dist/commands/observe.js +5 -2
  35. package/dist/commands/rebuild.d.ts +11 -0
  36. package/dist/commands/rebuild.js +12 -0
  37. package/dist/commands/reflect.d.ts +11 -0
  38. package/dist/commands/reflect.js +13 -0
  39. package/dist/commands/replay.d.ts +16 -0
  40. package/dist/commands/replay.js +14 -0
  41. package/dist/commands/setup.js +2 -2
  42. package/dist/commands/sleep.d.ts +1 -0
  43. package/dist/commands/sleep.js +29 -6
  44. package/dist/commands/status.js +6 -6
  45. package/dist/commands/sync-bd.d.ts +10 -0
  46. package/dist/commands/sync-bd.js +9 -0
  47. package/dist/commands/wake.js +53 -35
  48. package/dist/{context-COo8oq1k.d.ts → context-BUGaWpyL.d.ts} +1 -0
  49. package/dist/index.d.ts +55 -20
  50. package/dist/index.js +67 -16
  51. package/hooks/clawvault/HOOK.md +3 -2
  52. package/hooks/clawvault/handler.js +51 -0
  53. package/hooks/clawvault/handler.test.js +20 -0
  54. package/package.json +2 -2
@@ -1,6 +1,22 @@
1
+ import {
2
+ DATE_HEADING_RE,
3
+ inferObservationType,
4
+ normalizeObservationContent,
5
+ parseObservationLine,
6
+ parseObservationMarkdown,
7
+ renderObservationMarkdown,
8
+ renderScoredObservationLine
9
+ } from "./chunk-K6XHCUFL.js";
10
+ import {
11
+ ensureLedgerStructure,
12
+ ensureParentDir,
13
+ getLegacyObservationPath,
14
+ getObservationPath,
15
+ getRawTranscriptPath,
16
+ toDateKey
17
+ } from "./chunk-Z2XBWN7A.js";
18
+
1
19
  // src/observer/compressor.ts
2
- var DATE_HEADING_RE = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
3
- var OBSERVATION_LINE_RE = /^(🔴|🟡|🟢)\s+(.+)$/u;
4
20
  var CRITICAL_RE = /(?:\b(?:decision|decided|chose|chosen|selected|picked|opted|switched to)\s*:?|\bdecid(?:e|ed|ing|ion)\b|\berror\b|\bfail(?:ed|ure|ing)?\b|\bblock(?:ed|er)?\b|\bbreaking(?:\s+change)?s?\b|\bcritical\b|\b\w+\s+chosen\s+(?:for|over|as)\b|\bpublish(?:ed)?\b.*@?\d+\.\d+|\bmerge[d]?\s+(?:PR|pull\s+request)\b|\bshipped\b|\breleased?\b.*v?\d+\.\d+|\bsigned\b.*\b(?:contract|agreement|deal)\b|\bpricing\b.*\$|\bdemo\b.*\b(?:completed?|done|finished)\b|\bmeeting\b.*\b(?:completed?|done|finished)\b|\bstrategy\b.*\b(?:pivot|change|shift)\b)/i;
5
21
  var DEADLINE_WITH_DATE_RE = /(?:(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b).*(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2})|(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2}).*(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b))/i;
6
22
  var NOTABLE_RE = /\b(prefer(?:ence|s)?|likes?|dislikes?|context|pattern|architecture|approach|trade[- ]?off|milestone|stakeholder|teammate|collaborat(?:e|ed|ion)|discussion|notable|deadline|due|timeline|deploy(?:ed|ment)?|built|configured|launched|proposal|pitch|onboard(?:ed|ing)?|migrat(?:e|ed|ion)|domain|DNS|infra(?:structure)?)\b/i;
@@ -53,11 +69,12 @@ var Compressor = class {
53
69
  "Rules:",
54
70
  "- Output markdown only.",
55
71
  "- Group observations by date heading: ## YYYY-MM-DD",
56
- "- Each line must follow: <emoji> <HH:MM> <observation>",
57
- "- Priority emojis: \u{1F534} critical, \u{1F7E1} notable, \u{1F7E2} info",
58
- "- \u{1F534} for: decisions, blockers, deadlines, breaking changes, commitments to people, version releases, merged PRs, shipped features, client meetings/demos, pricing/contract decisions, strategy changes, signed agreements",
59
- "- \u{1F7E1} for: preferences, architecture discussions, trade-offs, milestones, people interactions, notable context, production deploys, new tools built, config changes, proposals sent, content published, infrastructure changes",
60
- "- \u{1F7E2} for: routine tasks, intermediate build steps, general progress, minor fixes",
72
+ "- Each observation line MUST follow: - [type|c=<0.00-1.00>|i=<0.00-1.00>] <observation>",
73
+ "- Allowed type tags: decision, preference, fact, commitment, milestone, lesson, relationship, project",
74
+ "- i >= 0.80 for structural/persistent observations (major decisions, blockers, releases, commitments)",
75
+ "- i 0.40-0.79 for potentially important observations (notable context, preferences, milestones)",
76
+ "- i < 0.40 for contextual/routine observations",
77
+ "- Confidence c reflects extraction certainty, not importance.",
61
78
  "- Preserve source tags when present (e.g., [main], [telegram-dm], [discord], [telegram-group]).",
62
79
  "",
63
80
  "QUALITY FILTERS (important):",
@@ -74,19 +91,16 @@ var Compressor = class {
74
91
  "",
75
92
  "PROJECT MILESTONES (critical \u2014 these are the most valuable observations):",
76
93
  "Projects are NOT just code. Milestones include business, strategy, client, and operational events.",
77
- "- \u{1F534} ALWAYS for: version releases/publishes, PR merges, shipped features, signed contracts, pricing decisions, client meetings/demos, strategy pivots, partnership agreements, major config changes",
78
- "- \u{1F7E1} ALWAYS for: production deploys, new tools/commands built, pitch decks sent, proposals delivered, infrastructure changes, domain/DNS changes, onboarding steps, content published (blog posts, landing pages)",
94
+ "- Use milestone/decision/commitment types for strategic events with high importance.",
95
+ "- Use preference/lesson/relationship/project/fact when appropriate.",
79
96
  "- Examples:",
80
- ' "\u{1F534} 14:00 Published clawvault@2.1.0 to npm with active session observer"',
81
- ' "\u{1F534} 14:00 Artemisa demo completed \u2014 #1 pain point confirmed: quick estimates"',
82
- ' "\u{1F534} 14:00 Pricing decision: $33K one-time + $3K/mo for Artemisa"',
83
- ' "\u{1F534} 14:00 Merged PR #2: Active session observation into master"',
84
- ' "\u{1F7E1} 14:00 Deployed pitch deck to artemisa-pitch-deck.vercel.app"',
85
- ' "\u{1F7E1} 14:00 Moved docs.clawvault.dev domain to new Vercel project"',
97
+ ' "- [decision|c=0.95|i=0.90] 14:00 Pricing decision: $33K one-time + $3K/mo for Artemisa"',
98
+ ' "- [milestone|c=0.93|i=0.88] 14:00 Published clawvault@2.1.0 to npm"',
99
+ ' "- [project|c=0.84|i=0.58] 14:00 Deployed pitch deck to artemisa-pitch-deck.vercel.app"',
86
100
  "- Do NOT collapse multiple milestones into one line \u2014 each matters for history.",
87
101
  "",
88
102
  "COMMITMENT FORMAT (when someone promises/agrees to something):",
89
- '- Use: "\u{1F534} HH:MM [COMMITMENT] <who> committed to <what> by <when>" (include deadline if mentioned)',
103
+ '- Prefer: "- [commitment|c=...|i=...] HH:MM [COMMITMENT] <who> committed to <what> by <when>"',
90
104
  "",
91
105
  "Keep observations concise and factual. Aim for signal, not completeness.",
92
106
  "",
@@ -179,7 +193,7 @@ var Compressor = class {
179
193
  }
180
194
  const cleaned = output.replace(/^```(?:markdown)?\s*/i, "").replace(/\s*```$/, "").trim();
181
195
  const lines = cleaned.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
182
- const hasObservationLine = lines.some((line) => OBSERVATION_LINE_RE.test(line));
196
+ const hasObservationLine = lines.some((line) => line.startsWith("- [") || /^(?:-\s*)?(🔴|🟡|🟢)\s+/.test(line));
183
197
  if (!hasObservationLine) {
184
198
  return "";
185
199
  }
@@ -188,7 +202,7 @@ var Compressor = class {
188
202
 
189
203
  ${cleaned}`;
190
204
  const sanitized = this.sanitizeWikiLinks(result);
191
- return this.enforcePriorityRules(sanitized);
205
+ return this.enforceImportanceRules(sanitized);
192
206
  }
193
207
  /**
194
208
  * Fix wiki-link corruption from LLM compression.
@@ -204,24 +218,43 @@ ${cleaned}`;
204
218
  result = result.replace(/ {2,}/g, " ");
205
219
  return result;
206
220
  }
207
- /**
208
- * Post-process LLM output to enforce priority rules.
209
- * Lines matching critical rules get upgraded to 🔴, notable rules to 🟡.
210
- */
211
- enforcePriorityRules(markdown) {
212
- return markdown.split(/\r?\n/).map((line) => {
213
- const match = line.match(OBSERVATION_LINE_RE);
214
- if (!match) return line;
215
- const currentPriority = match[1];
216
- const content = match[2];
217
- if (this.isCriticalContent(content) && currentPriority !== "\u{1F534}") {
218
- return line.replace(/^🟡|^🟢/u, "\u{1F534}");
219
- }
220
- if (this.isNotableContent(content) && currentPriority === "\u{1F7E2}") {
221
- return line.replace(/^🟢/u, "\u{1F7E1}");
221
+ enforceImportanceRules(markdown) {
222
+ const parsed = parseObservationMarkdown(markdown);
223
+ if (parsed.length === 0) {
224
+ return "";
225
+ }
226
+ const grouped = /* @__PURE__ */ new Map();
227
+ for (const record of parsed) {
228
+ const adjusted = this.enforceImportanceForRecord(record);
229
+ const bucket = grouped.get(record.date) ?? [];
230
+ bucket.push(adjusted);
231
+ grouped.set(record.date, bucket);
232
+ }
233
+ return renderObservationMarkdown(grouped);
234
+ }
235
+ enforceImportanceForRecord(record) {
236
+ let importance = record.importance;
237
+ let confidence = record.confidence;
238
+ let type = record.type;
239
+ if (this.isCriticalContent(record.content)) {
240
+ importance = Math.max(importance, 0.85);
241
+ confidence = Math.max(confidence, 0.85);
242
+ if (type === "fact") {
243
+ type = inferObservationType(record.content);
222
244
  }
223
- return line;
224
- }).join("\n");
245
+ } else if (this.isNotableContent(record.content)) {
246
+ importance = Math.max(importance, 0.5);
247
+ confidence = Math.max(confidence, 0.75);
248
+ }
249
+ if (type === "decision" || type === "commitment" || type === "milestone") {
250
+ importance = Math.max(importance, 0.6);
251
+ }
252
+ return {
253
+ type,
254
+ confidence: this.clamp01(confidence),
255
+ importance: this.clamp01(importance),
256
+ content: record.content
257
+ };
225
258
  }
226
259
  fallbackCompression(messages) {
227
260
  const sections = /* @__PURE__ */ new Map();
@@ -231,107 +264,96 @@ ${cleaned}`;
231
264
  if (!normalized) continue;
232
265
  const date = this.extractDate(message) ?? this.formatDate(this.now());
233
266
  const time = this.extractTime(message) ?? this.formatTime(this.now());
234
- const priority = this.inferPriority(normalized);
235
267
  const line = `${time} ${normalized}`;
236
- const dedupeKey = `${date}|${priority}|${this.normalizeText(line)}`;
268
+ const type = inferObservationType(line);
269
+ const importance = this.inferImportance(line, type);
270
+ const confidence = this.inferConfidence(line, type, importance);
271
+ const dedupeKey = `${date}|${type}|${normalizeObservationContent(line)}`;
237
272
  if (seen.has(dedupeKey)) continue;
238
273
  seen.add(dedupeKey);
239
274
  const bucket = sections.get(date) ?? [];
240
- bucket.push({ priority, content: line });
275
+ bucket.push({ type, confidence, importance, content: line });
241
276
  sections.set(date, bucket);
242
277
  }
243
278
  if (sections.size === 0) {
244
279
  const date = this.formatDate(this.now());
245
- sections.set(date, [{ priority: "\u{1F7E2}", content: `${this.formatTime(this.now())} Processed session updates.` }]);
280
+ sections.set(date, [{
281
+ type: "fact",
282
+ confidence: 0.7,
283
+ importance: 0.2,
284
+ content: `${this.formatTime(this.now())} Processed session updates.`
285
+ }]);
246
286
  }
247
287
  return this.renderSections(sections);
248
288
  }
249
289
  mergeObservations(existing, incoming) {
250
- const existingSections = this.parseSections(existing);
251
- const incomingSections = this.parseSections(incoming);
252
- if (incomingSections.size === 0) {
290
+ const existingRecords = parseObservationMarkdown(existing);
291
+ const incomingRecords = parseObservationMarkdown(incoming);
292
+ if (incomingRecords.length === 0) {
253
293
  return existing.trim();
254
294
  }
255
- if (existingSections.size === 0) {
256
- return this.renderSections(incomingSections);
257
- }
258
- for (const [date, lines] of existingSections.entries()) {
259
- existingSections.set(date, this.deduplicateObservationLines(lines));
260
- }
261
- for (const [date, lines] of incomingSections.entries()) {
262
- const current = this.deduplicateObservationLines(existingSections.get(date) ?? []);
263
- const seen = new Set(current.map((line) => this.normalizeObservationContent(line.content)));
264
- for (const line of lines) {
265
- const normalized = this.normalizeObservationContent(line.content);
266
- if (!normalized || seen.has(normalized)) {
267
- continue;
268
- }
269
- seen.add(normalized);
270
- current.push(line);
271
- }
272
- existingSections.set(date, current);
295
+ const merged = /* @__PURE__ */ new Map();
296
+ for (const record of existingRecords) {
297
+ this.mergeRecord(merged, {
298
+ date: record.date,
299
+ type: record.type,
300
+ confidence: record.confidence,
301
+ importance: record.importance,
302
+ content: record.content
303
+ });
273
304
  }
274
- return this.renderSections(existingSections);
275
- }
276
- deduplicateObservationLines(lines) {
277
- const deduped = [];
278
- const seen = /* @__PURE__ */ new Set();
279
- for (const line of lines) {
280
- const normalized = this.normalizeObservationContent(line.content);
281
- if (!normalized || seen.has(normalized)) {
282
- continue;
283
- }
284
- seen.add(normalized);
285
- deduped.push(line);
305
+ for (const record of incomingRecords) {
306
+ this.mergeRecord(merged, {
307
+ date: record.date,
308
+ type: record.type,
309
+ confidence: record.confidence,
310
+ importance: record.importance,
311
+ content: record.content
312
+ });
286
313
  }
287
- return deduped;
314
+ return this.renderSections(merged);
288
315
  }
289
- normalizeObservationContent(content) {
290
- return content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\s+/g, " ").trim().toLowerCase();
291
- }
292
- parseSections(markdown) {
293
- const sections = /* @__PURE__ */ new Map();
294
- let currentDate = null;
295
- for (const rawLine of markdown.split(/\r?\n/)) {
296
- const dateMatch = rawLine.match(DATE_HEADING_RE);
297
- if (dateMatch) {
298
- currentDate = dateMatch[1];
299
- if (!sections.has(currentDate)) {
300
- sections.set(currentDate, []);
301
- }
302
- continue;
303
- }
304
- if (!currentDate) continue;
305
- const lineMatch = rawLine.match(OBSERVATION_LINE_RE);
306
- if (!lineMatch) continue;
307
- const bucket = sections.get(currentDate) ?? [];
316
+ mergeRecord(sections, input) {
317
+ const bucket = sections.get(input.date) ?? [];
318
+ const key = normalizeObservationContent(input.content);
319
+ const index = bucket.findIndex((line) => normalizeObservationContent(line.content) === key);
320
+ if (index === -1) {
308
321
  bucket.push({
309
- priority: lineMatch[1],
310
- content: lineMatch[2].trim()
322
+ type: input.type,
323
+ confidence: this.clamp01(input.confidence),
324
+ importance: this.clamp01(input.importance),
325
+ content: input.content.trim()
311
326
  });
312
- sections.set(currentDate, bucket);
327
+ sections.set(input.date, bucket);
328
+ return;
313
329
  }
314
- return sections;
330
+ const existing = bucket[index];
331
+ bucket[index] = {
332
+ type: input.importance >= existing.importance ? input.type : existing.type,
333
+ confidence: this.clamp01(Math.max(existing.confidence, input.confidence)),
334
+ importance: this.clamp01(Math.max(existing.importance, input.importance)),
335
+ content: existing.content.length >= input.content.length ? existing.content : input.content
336
+ };
337
+ sections.set(input.date, bucket);
315
338
  }
316
339
  renderSections(sections) {
317
- const chunks = [];
318
- const sortedDates = [...sections.keys()].sort((a, b) => a.localeCompare(b));
319
- for (const date of sortedDates) {
320
- const lines = sections.get(date) ?? [];
321
- if (lines.length === 0) continue;
322
- chunks.push(`## ${date}`);
323
- chunks.push("");
324
- for (const line of lines) {
325
- chunks.push(`${line.priority} ${line.content}`);
326
- }
327
- chunks.push("");
340
+ return renderObservationMarkdown(sections);
341
+ }
342
+ inferImportance(text, type) {
343
+ if (this.isCriticalContent(text)) return 0.9;
344
+ if (this.isNotableContent(text)) return 0.6;
345
+ if (type === "decision" || type === "commitment" || type === "milestone") return 0.55;
346
+ if (type === "preference" || type === "lesson" || type === "relationship" || type === "project") return 0.45;
347
+ return 0.2;
348
+ }
349
+ inferConfidence(text, type, importance) {
350
+ let confidence = 0.72;
351
+ if (importance >= 0.8) confidence += 0.12;
352
+ if (type === "decision" || type === "commitment" || type === "milestone") confidence += 0.06;
353
+ if (/\b(?:decided|chose|committed|deadline|released|merged)\b/i.test(text)) {
354
+ confidence += 0.05;
328
355
  }
329
- return chunks.join("\n").trim();
330
- }
331
- inferPriority(text) {
332
- if (this.isCriticalContent(text)) return "\u{1F534}";
333
- if (this.isNotableContent(text)) return "\u{1F7E1}";
334
- return "\u{1F7E2}";
356
+ return this.clamp01(confidence);
335
357
  }
336
358
  isCriticalContent(text) {
337
359
  return CRITICAL_RE.test(text) || DEADLINE_WITH_DATE_RE.test(text);
@@ -359,11 +381,17 @@ ${cleaned}`;
359
381
  formatTime(date) {
360
382
  return date.toISOString().slice(11, 16);
361
383
  }
384
+ clamp01(value) {
385
+ if (!Number.isFinite(value)) return 0;
386
+ if (value < 0) return 0;
387
+ if (value > 1) return 1;
388
+ return value;
389
+ }
362
390
  };
363
391
 
364
392
  // src/observer/reflector.ts
365
393
  var DATE_HEADING_RE2 = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
366
- var OBSERVATION_LINE_RE2 = /^(🔴|🟡|🟢)\s+(.+)$/u;
394
+ var OBSERVATION_LINE_RE = /^(🔴|🟡|🟢)\s+(.+)$/u;
367
395
  var Reflector = class {
368
396
  now;
369
397
  constructor(options = {}) {
@@ -431,7 +459,7 @@ var Reflector = class {
431
459
  continue;
432
460
  }
433
461
  if (!currentDate) continue;
434
- const lineMatch = rawLine.match(OBSERVATION_LINE_RE2);
462
+ const lineMatch = rawLine.match(OBSERVATION_LINE_RE);
435
463
  if (!lineMatch) continue;
436
464
  const bucket = sections.get(currentDate) ?? [];
437
465
  bucket.push({
@@ -524,8 +552,16 @@ var CATEGORY_PATTERNS = [
524
552
  ]
525
553
  }
526
554
  ];
527
- var OBSERVATION_LINE_RE3 = /^(🔴|🟡|🟢)\s+(\d{2}:\d{2})?\s*(.+)$/u;
528
- var DATE_HEADING_RE3 = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
555
+ var TYPE_TO_CATEGORY = {
556
+ decision: "decisions",
557
+ preference: "preferences",
558
+ fact: "facts",
559
+ commitment: "commitments",
560
+ milestone: "projects",
561
+ lesson: "lessons",
562
+ relationship: "people",
563
+ project: "projects"
564
+ };
529
565
  var Router = class {
530
566
  vaultPath;
531
567
  constructor(vaultPath) {
@@ -533,17 +569,25 @@ var Router = class {
533
569
  }
534
570
  /**
535
571
  * Takes observation markdown and routes items to appropriate vault categories.
536
- * Only routes 🔴 and 🟡 items 🟢 stays only in observations.
572
+ * Routes only items with importance >= 0.4.
537
573
  * Returns a summary of what was routed where.
538
574
  */
539
575
  route(observationMarkdown) {
540
576
  const items = this.parseObservations(observationMarkdown);
541
577
  const routed = [];
542
578
  for (const item of items) {
543
- if (item.priority === "\u{1F7E2}") continue;
544
- const category = this.categorize(item.content);
579
+ if (item.importance < 0.4) continue;
580
+ const category = this.categorize(item.type, item.content);
545
581
  if (!category) continue;
546
- const routedItem = { category, title: item.title, content: item.content, priority: item.priority, date: item.date };
582
+ const routedItem = {
583
+ category,
584
+ title: item.title,
585
+ content: item.content,
586
+ type: item.type,
587
+ confidence: item.confidence,
588
+ importance: item.importance,
589
+ date: item.date
590
+ };
547
591
  routed.push(routedItem);
548
592
  this.appendToCategory(category, routedItem);
549
593
  }
@@ -551,24 +595,21 @@ var Router = class {
551
595
  return { routed, summary };
552
596
  }
553
597
  parseObservations(markdown) {
554
- const results = [];
555
- let currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
556
- for (const line of markdown.split(/\r?\n/)) {
557
- const dateMatch = line.match(DATE_HEADING_RE3);
558
- if (dateMatch) {
559
- currentDate = dateMatch[1];
560
- continue;
561
- }
562
- const obsMatch = line.match(OBSERVATION_LINE_RE3);
563
- if (!obsMatch) continue;
564
- const priority = obsMatch[1];
565
- const content = obsMatch[3].trim();
566
- const title = content.slice(0, 80).replace(/[^a-zA-Z0-9\s-]/g, "").trim();
567
- results.push({ priority, content, date: currentDate, title });
598
+ const records = parseObservationMarkdown(markdown);
599
+ return records.map((record) => ({
600
+ type: record.type,
601
+ confidence: record.confidence,
602
+ importance: record.importance,
603
+ content: record.content,
604
+ date: record.date,
605
+ title: record.content.slice(0, 80).replace(/[^a-zA-Z0-9\s-]/g, "").trim()
606
+ }));
607
+ }
608
+ categorize(type, content) {
609
+ const typedCategory = TYPE_TO_CATEGORY[type];
610
+ if (typedCategory) {
611
+ return typedCategory;
568
612
  }
569
- return results;
570
- }
571
- categorize(content) {
572
613
  for (const { category, patterns } of CATEGORY_PATTERNS) {
573
614
  if (patterns.some((p) => p.test(content))) {
574
615
  return category;
@@ -577,7 +618,9 @@ var Router = class {
577
618
  return null;
578
619
  }
579
620
  normalizeForDedup(content) {
580
- return content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\[\[[^\]]*\]\]/g, (m) => m.replace(/\[\[|\]\]/g, "")).replace(/\s+/g, " ").trim().toLowerCase();
621
+ return normalizeObservationContent(
622
+ content.replace(/\[\[[^\]]*\]\]/g, (match) => match.replace(/\[\[|\]\]/g, ""))
623
+ );
581
624
  }
582
625
  /**
583
626
  * Extract entity slug from observation content for people/projects routing.
@@ -634,12 +677,15 @@ var Router = class {
634
677
  const normalizedNew = this.normalizeForDedup(item.content);
635
678
  const existingLines = existing.split(/\r?\n/);
636
679
  for (const line of existingLines) {
637
- const lineContent = line.replace(/^-\s*(?:🔴|🟡|🟢)\s*/, "");
638
- if (this.normalizeForDedup(lineContent) === normalizedNew) return;
680
+ const lineContent = line.replace(/^-\s*/, "").trim();
681
+ const parsed = parseObservationLine(lineContent, item.date);
682
+ const candidate = parsed ? parsed.content : lineContent;
683
+ if (this.normalizeForDedup(candidate) === normalizedNew) return;
639
684
  }
640
685
  for (const line of existingLines) {
641
- const lineContent = line.replace(/^-\s*(?:🔴|🟡|🟢)\s*/, "");
642
- const normalizedExisting = this.normalizeForDedup(lineContent);
686
+ const lineContent = line.replace(/^-\s*/, "").trim();
687
+ const parsed = parseObservationLine(lineContent, item.date);
688
+ const normalizedExisting = this.normalizeForDedup(parsed ? parsed.content : lineContent);
643
689
  if (normalizedExisting.length > 10 && normalizedNew.length > 10) {
644
690
  const shorter = normalizedNew.length < normalizedExisting.length ? normalizedNew : normalizedExisting;
645
691
  const longer = normalizedNew.length >= normalizedExisting.length ? normalizedNew : normalizedExisting;
@@ -647,7 +693,12 @@ var Router = class {
647
693
  }
648
694
  }
649
695
  const linkedContent = this.addWikiLinks(item.content);
650
- const entry = `- ${item.priority} ${linkedContent}`;
696
+ const entry = renderScoredObservationLine({
697
+ type: item.type,
698
+ confidence: item.confidence,
699
+ importance: item.importance,
700
+ content: linkedContent
701
+ });
651
702
  const entitySlug = this.extractEntitySlug(item.content, category);
652
703
  const headerLabel = entitySlug ? `${category}/${entitySlug}` : category;
653
704
  const header = existing ? "" : `# ${headerLabel} \u2014 ${item.date}
@@ -842,44 +893,48 @@ ${entry}
842
893
  };
843
894
 
844
895
  // src/observer/observer.ts
845
- var DATE_HEADING_RE4 = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
846
- var OBSERVATION_LINE_RE4 = /^(🔴|🟡|🟢)\s+(.+)$/u;
847
896
  var Observer = class {
848
897
  vaultPath;
849
- observationsDir;
850
898
  tokenThreshold;
899
+ // Kept for backwards API compatibility with callers that still pass this.
900
+ // Reflection now runs explicitly via clawvault reflect.
851
901
  reflectThreshold;
852
902
  compressor;
853
903
  reflector;
854
904
  now;
905
+ rawCapture;
855
906
  router;
856
907
  pendingMessages = [];
857
908
  observationsCache = "";
858
909
  lastRoutingSummary = "";
859
910
  constructor(vaultPath, options = {}) {
860
911
  this.vaultPath = path2.resolve(vaultPath);
861
- this.observationsDir = path2.join(this.vaultPath, "observations");
862
912
  this.tokenThreshold = options.tokenThreshold ?? 3e4;
863
913
  this.reflectThreshold = options.reflectThreshold ?? 4e4;
864
914
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
865
915
  this.compressor = options.compressor ?? new Compressor({ model: options.model, now: this.now });
866
916
  this.reflector = options.reflector ?? new Reflector({ now: this.now });
917
+ this.rawCapture = options.rawCapture ?? true;
867
918
  this.router = new Router(vaultPath);
868
- fs2.mkdirSync(this.observationsDir, { recursive: true });
919
+ ensureLedgerStructure(this.vaultPath);
869
920
  this.observationsCache = this.readTodayObservations();
870
921
  }
871
- async processMessages(messages) {
922
+ async processMessages(messages, options = {}) {
872
923
  const incoming = messages.map((message) => message.trim()).filter(Boolean);
873
924
  if (incoming.length === 0) {
874
925
  return;
875
926
  }
927
+ if (this.rawCapture) {
928
+ this.persistRawMessages(incoming, options);
929
+ }
876
930
  this.pendingMessages.push(...incoming);
877
931
  const buffered = this.pendingMessages.join("\n");
878
932
  if (this.estimateTokens(buffered) < this.tokenThreshold) {
879
933
  return;
880
934
  }
881
- const todayPath = this.getObservationPath(this.now());
882
- const existingRaw = this.readObservationFile(todayPath);
935
+ const today = this.now();
936
+ const todayPath = getObservationPath(this.vaultPath, today);
937
+ const existingRaw = this.readObservationForDate(today);
883
938
  const existing = this.deduplicateObservationMarkdown(existingRaw);
884
939
  if (existingRaw.trim() !== existing) {
885
940
  this.writeObservationFile(todayPath, existing);
@@ -896,7 +951,6 @@ var Observer = class {
896
951
  if (summary) {
897
952
  this.lastRoutingSummary = summary;
898
953
  }
899
- await this.reflectIfNeeded();
900
954
  }
901
955
  /**
902
956
  * Force-flush pending messages regardless of threshold.
@@ -906,8 +960,9 @@ var Observer = class {
906
960
  if (this.pendingMessages.length === 0) {
907
961
  return { observations: this.observationsCache, routingSummary: this.lastRoutingSummary };
908
962
  }
909
- const todayPath = this.getObservationPath(this.now());
910
- const existingRaw = this.readObservationFile(todayPath);
963
+ const today = this.now();
964
+ const todayPath = getObservationPath(this.vaultPath, today);
965
+ const existingRaw = this.readObservationForDate(today);
911
966
  const existing = this.deduplicateObservationMarkdown(existingRaw);
912
967
  if (existingRaw.trim() !== existing) {
913
968
  this.writeObservationFile(todayPath, existing);
@@ -920,7 +975,6 @@ var Observer = class {
920
975
  this.observationsCache = compressed;
921
976
  const { summary } = this.router.route(compressed);
922
977
  this.lastRoutingSummary = summary;
923
- await this.reflectIfNeeded();
924
978
  }
925
979
  return { observations: this.observationsCache, routingSummary: this.lastRoutingSummary };
926
980
  }
@@ -931,13 +985,16 @@ var Observer = class {
931
985
  estimateTokens(input) {
932
986
  return Math.ceil(input.length / 4);
933
987
  }
934
- getObservationPath(date) {
935
- const datePart = date.toISOString().split("T")[0];
936
- return path2.join(this.observationsDir, `${datePart}.md`);
937
- }
938
988
  readTodayObservations() {
939
- const todayPath = this.getObservationPath(this.now());
940
- return this.readObservationFile(todayPath);
989
+ return this.readObservationForDate(this.now());
990
+ }
991
+ readObservationForDate(date) {
992
+ const ledgerPath = getObservationPath(this.vaultPath, date);
993
+ const ledgerValue = this.readObservationFile(ledgerPath);
994
+ if (ledgerValue) {
995
+ return ledgerValue;
996
+ }
997
+ return this.readObservationFile(getLegacyObservationPath(this.vaultPath, toDateKey(date)));
941
998
  }
942
999
  readObservationFile(filePath) {
943
1000
  if (!fs2.existsSync(filePath)) {
@@ -946,292 +1003,69 @@ var Observer = class {
946
1003
  return fs2.readFileSync(filePath, "utf-8").trim();
947
1004
  }
948
1005
  writeObservationFile(filePath, content) {
949
- fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
1006
+ ensureParentDir(filePath);
950
1007
  fs2.writeFileSync(filePath, `${content.trim()}
951
1008
  `, "utf-8");
952
1009
  }
953
- getObservationFiles() {
954
- if (!fs2.existsSync(this.observationsDir)) {
955
- return [];
956
- }
957
- return fs2.readdirSync(this.observationsDir).filter((name) => name.endsWith(".md")).sort((a, b) => a.localeCompare(b)).map((name) => path2.join(this.observationsDir, name));
958
- }
959
- readObservationCorpus() {
960
- const files = this.getObservationFiles();
961
- if (files.length === 0) {
962
- return "";
963
- }
964
- return files.map((filePath) => this.readObservationFile(filePath)).filter(Boolean).join("\n\n");
965
- }
966
1010
  deduplicateObservationMarkdown(markdown) {
967
- const parsed = this.parseSections(markdown);
968
- if (parsed.size === 0) {
1011
+ const parsed = parseObservationMarkdown(markdown);
1012
+ if (parsed.length === 0) {
969
1013
  return markdown.trim();
970
1014
  }
971
- for (const [date, lines] of parsed.entries()) {
972
- const seen = /* @__PURE__ */ new Set();
973
- const deduped = [];
974
- for (const line of lines) {
975
- const normalized = this.normalizeObservationContent(line.content);
976
- if (!normalized || seen.has(normalized)) {
977
- continue;
978
- }
979
- seen.add(normalized);
980
- deduped.push(line);
981
- }
982
- parsed.set(date, deduped);
983
- }
984
- return this.renderSections(parsed);
985
- }
986
- parseSections(markdown) {
987
- const sections = /* @__PURE__ */ new Map();
988
- let currentDate = null;
989
- for (const rawLine of markdown.split(/\r?\n/)) {
990
- const dateMatch = rawLine.match(DATE_HEADING_RE4);
991
- if (dateMatch) {
992
- currentDate = dateMatch[1];
993
- if (!sections.has(currentDate)) {
994
- sections.set(currentDate, []);
995
- }
996
- continue;
997
- }
998
- if (!currentDate) {
999
- continue;
1000
- }
1001
- const lineMatch = rawLine.match(OBSERVATION_LINE_RE4);
1002
- if (!lineMatch) {
1003
- continue;
1015
+ const grouped = /* @__PURE__ */ new Map();
1016
+ for (const record of parsed) {
1017
+ const bucket = grouped.get(record.date) ?? [];
1018
+ const normalized = normalizeObservationContent(record.content);
1019
+ const existingIndex = bucket.findIndex(
1020
+ (line) => normalizeObservationContent(line.content) === normalized
1021
+ );
1022
+ if (existingIndex === -1) {
1023
+ bucket.push({
1024
+ type: record.type,
1025
+ confidence: record.confidence,
1026
+ importance: record.importance,
1027
+ content: record.content
1028
+ });
1029
+ } else {
1030
+ const existing = bucket[existingIndex];
1031
+ bucket[existingIndex] = {
1032
+ type: record.importance >= existing.importance ? record.type : existing.type,
1033
+ confidence: Math.max(existing.confidence, record.confidence),
1034
+ importance: Math.max(existing.importance, record.importance),
1035
+ content: existing.content.length >= record.content.length ? existing.content : record.content
1036
+ };
1004
1037
  }
1005
- const current = sections.get(currentDate) ?? [];
1006
- current.push({
1007
- priority: lineMatch[1],
1008
- content: lineMatch[2].trim()
1009
- });
1010
- sections.set(currentDate, current);
1038
+ grouped.set(record.date, bucket);
1011
1039
  }
1012
- return sections;
1013
- }
1014
- renderSections(sections) {
1015
- const chunks = [];
1016
- const dates = [...sections.keys()].sort((a, b) => a.localeCompare(b));
1017
- for (const date of dates) {
1018
- const lines = sections.get(date) ?? [];
1019
- if (lines.length === 0) {
1020
- continue;
1021
- }
1022
- chunks.push(`## ${date}`);
1023
- chunks.push("");
1024
- for (const line of lines) {
1025
- chunks.push(`${line.priority} ${line.content}`);
1026
- }
1027
- chunks.push("");
1028
- }
1029
- return chunks.join("\n").trim();
1030
- }
1031
- normalizeObservationContent(content) {
1032
- return content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\s+/g, " ").trim().toLowerCase();
1040
+ return renderObservationMarkdown(grouped);
1041
+ }
1042
+ persistRawMessages(messages, options) {
1043
+ const source = this.sanitizeSource(options.source ?? "openclaw");
1044
+ const messageTimestamp = options.timestamp ?? this.now();
1045
+ const rawPath = getRawTranscriptPath(this.vaultPath, source, messageTimestamp);
1046
+ ensureParentDir(rawPath);
1047
+ const records = messages.map((message) => JSON.stringify({
1048
+ recordedAt: this.now().toISOString(),
1049
+ timestamp: messageTimestamp.toISOString(),
1050
+ source,
1051
+ sessionKey: options.sessionKey ?? null,
1052
+ transcriptId: options.transcriptId ?? null,
1053
+ message
1054
+ }));
1055
+ fs2.appendFileSync(rawPath, `${records.join("\n")}
1056
+ `, "utf-8");
1033
1057
  }
1034
- async reflectIfNeeded() {
1035
- const corpus = this.readObservationCorpus();
1036
- if (this.estimateTokens(corpus) < this.reflectThreshold) {
1037
- return;
1038
- }
1039
- for (const filePath of this.getObservationFiles()) {
1040
- const current = this.readObservationFile(filePath);
1041
- if (!current) continue;
1042
- const reflected = this.reflector.reflect(current).trim();
1043
- if (!reflected) {
1044
- fs2.rmSync(filePath, { force: true });
1045
- continue;
1046
- }
1047
- this.writeObservationFile(filePath, reflected);
1058
+ sanitizeSource(source) {
1059
+ const normalized = source.trim().toLowerCase();
1060
+ if (/^[a-z0-9_-]{1,64}$/.test(normalized)) {
1061
+ return normalized;
1048
1062
  }
1049
- this.observationsCache = this.readTodayObservations();
1063
+ return "openclaw";
1050
1064
  }
1051
1065
  };
1052
1066
 
1053
- // src/observer/session-parser.ts
1054
- import * as fs3 from "fs";
1055
- import * as path3 from "path";
1056
- var JSONL_SAMPLE_LIMIT = 20;
1057
- var MARKDOWN_SIGNAL_RE = /^(#{1,6}\s|[-*+]\s|>\s)/;
1058
- var MARKDOWN_INLINE_RE = /(\[[^\]]+\]\([^)]+\)|[*_`~])/;
1059
- function normalizeText(value) {
1060
- return value.replace(/\s+/g, " ").trim();
1061
- }
1062
- function extractText(value) {
1063
- if (typeof value === "string") {
1064
- return normalizeText(value);
1065
- }
1066
- if (Array.isArray(value)) {
1067
- const parts = [];
1068
- for (const part of value) {
1069
- const extracted = extractText(part);
1070
- if (extracted) {
1071
- parts.push(extracted);
1072
- }
1073
- }
1074
- return normalizeText(parts.join(" "));
1075
- }
1076
- if (!value || typeof value !== "object") {
1077
- return "";
1078
- }
1079
- const record = value;
1080
- if (typeof record.text === "string") {
1081
- return normalizeText(record.text);
1082
- }
1083
- if (typeof record.content === "string") {
1084
- return normalizeText(record.content);
1085
- }
1086
- return "";
1087
- }
1088
- function normalizeRole(role) {
1089
- if (typeof role !== "string") {
1090
- return "";
1091
- }
1092
- const normalized = role.trim().toLowerCase();
1093
- if (!normalized) {
1094
- return "";
1095
- }
1096
- return normalized;
1097
- }
1098
- function isLikelyJsonMessage(value) {
1099
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1100
- return false;
1101
- }
1102
- const record = value;
1103
- if ("role" in record && "content" in record) {
1104
- return true;
1105
- }
1106
- if (record.type === "message" && record.message && typeof record.message === "object") {
1107
- return true;
1108
- }
1109
- return false;
1110
- }
1111
- function parseJsonLine(line) {
1112
- let parsed;
1113
- try {
1114
- parsed = JSON.parse(line);
1115
- } catch {
1116
- return "";
1117
- }
1118
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1119
- return "";
1120
- }
1121
- const entry = parsed;
1122
- if ("role" in entry && "content" in entry) {
1123
- const role = normalizeRole(entry.role);
1124
- const content = extractText(entry.content);
1125
- if (!content) return "";
1126
- return role ? `${role}: ${content}` : content;
1127
- }
1128
- if (entry.type === "message" && entry.message && typeof entry.message === "object") {
1129
- const message = entry.message;
1130
- const role = normalizeRole(message.role);
1131
- const content = extractText(message.content);
1132
- if (!content) return "";
1133
- return role ? `${role}: ${content}` : content;
1134
- }
1135
- return "";
1136
- }
1137
- function parseJsonLines(raw) {
1138
- const messages = [];
1139
- for (const line of raw.split(/\r?\n/)) {
1140
- const trimmed = line.trim();
1141
- if (!trimmed) continue;
1142
- const parsed = parseJsonLine(trimmed);
1143
- if (parsed) {
1144
- messages.push(parsed);
1145
- }
1146
- }
1147
- return messages;
1148
- }
1149
- function stripMarkdownSyntax(text) {
1150
- return normalizeText(
1151
- text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[*_`~]/g, "").replace(/<[^>]+>/g, "")
1152
- );
1153
- }
1154
- function normalizeMarkdownLine(line) {
1155
- return stripMarkdownSyntax(
1156
- line.replace(/^>\s*/, "").replace(/^[-*+]\s+/, "").replace(/^#{1,6}\s+/, "")
1157
- );
1158
- }
1159
- function parseMarkdown(raw) {
1160
- const withoutCodeBlocks = raw.replace(/```[\s\S]*?```/g, " ");
1161
- const blocks = withoutCodeBlocks.split(/\r?\n\s*\r?\n/).map((block) => block.trim()).filter(Boolean);
1162
- const messages = [];
1163
- for (const block of blocks) {
1164
- const lines = block.split(/\r?\n/).map((line) => normalizeMarkdownLine(line)).filter(Boolean);
1165
- if (lines.length === 0) {
1166
- continue;
1167
- }
1168
- const joined = stripMarkdownSyntax(lines.join(" "));
1169
- if (!joined) continue;
1170
- const roleMatch = /^(user|assistant|system|tool)\s*:?\s*(.+)$/i.exec(joined);
1171
- if (roleMatch) {
1172
- const role = normalizeRole(roleMatch[1]);
1173
- const content = normalizeText(roleMatch[2]);
1174
- if (content) {
1175
- messages.push(`${role}: ${content}`);
1176
- }
1177
- continue;
1178
- }
1179
- messages.push(joined);
1180
- }
1181
- return messages;
1182
- }
1183
- function parsePlainText(raw) {
1184
- return raw.split(/\r?\n/).map((line) => normalizeText(line)).filter(Boolean);
1185
- }
1186
- function detectSessionFormat(raw, filePath) {
1187
- const nonEmptyLines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1188
- if (nonEmptyLines.length === 0) {
1189
- return "plain";
1190
- }
1191
- const sample = nonEmptyLines.slice(0, JSONL_SAMPLE_LIMIT);
1192
- const jsonHits = sample.filter((line) => {
1193
- try {
1194
- const parsed = JSON.parse(line);
1195
- return isLikelyJsonMessage(parsed);
1196
- } catch {
1197
- return false;
1198
- }
1199
- }).length;
1200
- if (jsonHits >= Math.max(1, Math.ceil(sample.length * 0.6))) {
1201
- return "jsonl";
1202
- }
1203
- const ext = path3.extname(filePath).toLowerCase();
1204
- if (ext === ".md" || ext === ".markdown") {
1205
- return "markdown";
1206
- }
1207
- const markdownSignals = sample.filter((line) => MARKDOWN_SIGNAL_RE.test(line) || MARKDOWN_INLINE_RE.test(line)).length;
1208
- if (markdownSignals >= Math.max(2, Math.ceil(sample.length * 0.4))) {
1209
- return "markdown";
1210
- }
1211
- return "plain";
1212
- }
1213
- function parseSessionFile(filePath) {
1214
- const resolved = path3.resolve(filePath);
1215
- const raw = fs3.readFileSync(resolved, "utf-8");
1216
- const format = detectSessionFormat(raw, resolved);
1217
- if (format === "jsonl") {
1218
- const parsed = parseJsonLines(raw);
1219
- if (parsed.length > 0) {
1220
- return parsed;
1221
- }
1222
- }
1223
- if (format === "markdown") {
1224
- const parsed = parseMarkdown(raw);
1225
- if (parsed.length > 0) {
1226
- return parsed;
1227
- }
1228
- }
1229
- return parsePlainText(raw);
1230
- }
1231
-
1232
1067
  export {
1233
1068
  Compressor,
1234
1069
  Reflector,
1235
- Observer,
1236
- parseSessionFile
1070
+ Observer
1237
1071
  };