clawvault 1.9.5 → 1.10.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.
- package/README.md +41 -24
- package/dist/{chunk-RTIBMFCQ.js → chunk-2MP4EHJ7.js} +61 -7
- package/dist/{chunk-ITN7XQPX.js → chunk-SIDM2I2C.js} +19 -10
- package/dist/{chunk-AVPHNEDB.js → chunk-UBRYOIII.js} +122 -24
- package/dist/commands/context.js +1 -1
- package/dist/commands/observe.js +2 -2
- package/dist/commands/sleep.js +1 -1
- package/dist/commands/wake.js +35 -2
- package/dist/index.d.ts +9 -1
- package/dist/index.js +3 -3
- package/package.json +1 -1
- package/dashboard/test-crash.mjs +0 -37
- package/dashboard/test-screenshot.png +0 -0
package/README.md
CHANGED
|
@@ -31,12 +31,6 @@ bun install -g qmd # or: npm install -g qmd
|
|
|
31
31
|
npm install -g clawvault
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
## Blog & Resources
|
|
35
|
-
|
|
36
|
-
- **Blog:** [clawvault.dev/blog](https://clawvault.dev/blog/)
|
|
37
|
-
- **RSS:** [feed.xml](https://clawvault.dev/blog/feed.xml)
|
|
38
|
-
- **Sitemap:** [sitemap.xml](https://clawvault.dev/sitemap.xml)
|
|
39
|
-
|
|
40
34
|
## Why ClawVault?
|
|
41
35
|
|
|
42
36
|
AI agents forget things. Context windows overflow, sessions end, important details get lost. ClawVault fixes that:
|
|
@@ -75,6 +69,43 @@ echo 'export CLAWVAULT_PATH="$HOME/memory"' >> ~/.bashrc
|
|
|
75
69
|
eval "$(clawvault shell-init)"
|
|
76
70
|
```
|
|
77
71
|
|
|
72
|
+
## Observational Memory
|
|
73
|
+
|
|
74
|
+
Automatically compress conversations into prioritized observations:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# One-shot: compress a conversation file
|
|
78
|
+
clawvault observe --compress session.md
|
|
79
|
+
|
|
80
|
+
# Watch mode: monitor a directory for new session files
|
|
81
|
+
clawvault observe --watch ./sessions/
|
|
82
|
+
|
|
83
|
+
# Background daemon
|
|
84
|
+
clawvault observe --daemon
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Observations use emoji priorities:
|
|
88
|
+
- 🔴 **Critical** — decisions, errors, blockers, deadlines
|
|
89
|
+
- 🟡 **Notable** — preferences, architecture discussions, people interactions
|
|
90
|
+
- 🟢 **Info** — routine updates, deployments, general progress
|
|
91
|
+
|
|
92
|
+
Critical and notable observations are automatically routed to vault categories (`decisions/`, `lessons/`, `people/`, etc.). The system uses LLM compression (Gemini, Anthropic, or OpenAI) with a rule-based fallback.
|
|
93
|
+
|
|
94
|
+
Integrated into the sleep/wake lifecycle:
|
|
95
|
+
```bash
|
|
96
|
+
clawvault sleep "task summary" --session-transcript conversation.md
|
|
97
|
+
# → observations auto-generated and routed
|
|
98
|
+
|
|
99
|
+
clawvault wake
|
|
100
|
+
# → recent 🔴/🟡 observations included in context
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Token-budget-aware context injection:
|
|
104
|
+
```bash
|
|
105
|
+
clawvault context "what decisions were made" --budget 2000
|
|
106
|
+
# → fits within token budget, 🔴 items first
|
|
107
|
+
```
|
|
108
|
+
|
|
78
109
|
## ClawVault Cloud
|
|
79
110
|
|
|
80
111
|
ClawVault Cloud extends local memory with org-linked decision traces. The local vault stays your source of truth, and cloud sync adds cross-agent visibility plus centralized audit trails.
|
|
@@ -101,23 +132,13 @@ clawvault trace emit --summary "Approved 20% discount for ACME"
|
|
|
101
132
|
clawvault sync
|
|
102
133
|
```
|
|
103
134
|
|
|
104
|
-
## Search
|
|
135
|
+
## Search
|
|
105
136
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
| Tool | Backend | Speed | API Limits |
|
|
109
|
-
|------|---------|-------|------------|
|
|
110
|
-
| `qmd search` / `clawvault search` | Local BM25 | Instant | None |
|
|
111
|
-
| `qmd vsearch` / `clawvault vsearch` | Local embeddings | Fast | None |
|
|
112
|
-
| `memory_search` | Gemini API | Variable | **Yes, hits quotas** |
|
|
137
|
+
ClawVault provides local search via qmd (BM25 + semantic). Works alongside OpenClaw's built-in `memory_search`.
|
|
113
138
|
|
|
114
139
|
```bash
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
clawvault search "query"
|
|
118
|
-
|
|
119
|
-
# ❌ Avoid (API quotas)
|
|
120
|
-
memory_search
|
|
140
|
+
clawvault search "query" # BM25 keyword search
|
|
141
|
+
clawvault vsearch "what did I decide" # Semantic search (local embeddings)
|
|
121
142
|
```
|
|
122
143
|
|
|
123
144
|
## Vault Structure
|
|
@@ -250,10 +271,6 @@ clawvault sleep "..." --next "..."
|
|
|
250
271
|
clawvault checkpoint --working-on "..." --focus "..." --blocked "..."
|
|
251
272
|
\`\`\`
|
|
252
273
|
|
|
253
|
-
### Why qmd over memory_search?
|
|
254
|
-
- Local embeddings — no API quotas
|
|
255
|
-
- Always works — no external dependencies
|
|
256
|
-
- Fast — instant BM25, quick semantic
|
|
257
274
|
```
|
|
258
275
|
|
|
259
276
|
## Templates
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Observer,
|
|
3
3
|
parseSessionFile
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-SIDM2I2C.js";
|
|
5
5
|
|
|
6
6
|
// src/commands/observe.ts
|
|
7
7
|
import * as fs2 from "fs";
|
|
@@ -12,21 +12,25 @@ import { spawn } from "child_process";
|
|
|
12
12
|
import * as fs from "fs";
|
|
13
13
|
import * as path from "path";
|
|
14
14
|
import chokidar from "chokidar";
|
|
15
|
+
var DEFAULT_FLUSH_THRESHOLD_CHARS = 500;
|
|
15
16
|
var SessionWatcher = class {
|
|
16
17
|
watchPath;
|
|
17
18
|
observer;
|
|
18
19
|
ignoreInitial;
|
|
19
20
|
debounceMs;
|
|
21
|
+
flushThresholdChars;
|
|
20
22
|
watcher = null;
|
|
21
23
|
fileOffsets = /* @__PURE__ */ new Map();
|
|
22
24
|
pendingPaths = /* @__PURE__ */ new Set();
|
|
23
25
|
debounceTimer = null;
|
|
24
26
|
processingQueue = Promise.resolve();
|
|
27
|
+
bufferedChars = 0;
|
|
25
28
|
constructor(watchPath, observer, options = {}) {
|
|
26
29
|
this.watchPath = path.resolve(watchPath);
|
|
27
30
|
this.observer = observer;
|
|
28
31
|
this.ignoreInitial = options.ignoreInitial ?? false;
|
|
29
32
|
this.debounceMs = options.debounceMs ?? 500;
|
|
33
|
+
this.flushThresholdChars = Math.max(1, options.flushThresholdChars ?? DEFAULT_FLUSH_THRESHOLD_CHARS);
|
|
30
34
|
}
|
|
31
35
|
async start() {
|
|
32
36
|
if (!fs.existsSync(this.watchPath)) {
|
|
@@ -55,14 +59,22 @@ var SessionWatcher = class {
|
|
|
55
59
|
this.watcher?.once("ready", () => resolve3());
|
|
56
60
|
this.watcher?.once("error", (error) => reject(error));
|
|
57
61
|
});
|
|
62
|
+
if (this.ignoreInitial) {
|
|
63
|
+
this.primeInitialOffsets();
|
|
64
|
+
}
|
|
58
65
|
}
|
|
59
66
|
async stop() {
|
|
60
67
|
if (this.debounceTimer) {
|
|
61
68
|
clearTimeout(this.debounceTimer);
|
|
62
69
|
this.debounceTimer = null;
|
|
70
|
+
this.drainPendingPaths();
|
|
63
71
|
}
|
|
64
|
-
this.pendingPaths.clear();
|
|
65
72
|
await this.processingQueue.catch(() => void 0);
|
|
73
|
+
if (this.bufferedChars > 0) {
|
|
74
|
+
await this.observer.flush();
|
|
75
|
+
this.bufferedChars = 0;
|
|
76
|
+
}
|
|
77
|
+
this.pendingPaths.clear();
|
|
66
78
|
await this.watcher?.close();
|
|
67
79
|
this.watcher = null;
|
|
68
80
|
}
|
|
@@ -72,13 +84,16 @@ var SessionWatcher = class {
|
|
|
72
84
|
}
|
|
73
85
|
this.debounceTimer = setTimeout(() => {
|
|
74
86
|
this.debounceTimer = null;
|
|
75
|
-
|
|
76
|
-
this.pendingPaths.clear();
|
|
77
|
-
for (const changedPath of nextPaths) {
|
|
78
|
-
this.processingQueue = this.processingQueue.then(() => this.consumeFile(changedPath)).catch(() => void 0);
|
|
79
|
-
}
|
|
87
|
+
this.drainPendingPaths();
|
|
80
88
|
}, this.debounceMs);
|
|
81
89
|
}
|
|
90
|
+
drainPendingPaths() {
|
|
91
|
+
const nextPaths = [...this.pendingPaths];
|
|
92
|
+
this.pendingPaths.clear();
|
|
93
|
+
for (const changedPath of nextPaths) {
|
|
94
|
+
this.processingQueue = this.processingQueue.then(() => this.consumeFile(changedPath)).catch(() => void 0);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
82
97
|
async consumeFile(filePath) {
|
|
83
98
|
const resolved = path.resolve(filePath);
|
|
84
99
|
if (!fs.existsSync(resolved)) {
|
|
@@ -109,6 +124,45 @@ var SessionWatcher = class {
|
|
|
109
124
|
return;
|
|
110
125
|
}
|
|
111
126
|
await this.observer.processMessages(messages);
|
|
127
|
+
this.bufferedChars += chunk.length;
|
|
128
|
+
if (this.bufferedChars >= this.flushThresholdChars) {
|
|
129
|
+
await this.observer.flush();
|
|
130
|
+
this.bufferedChars = 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
primeInitialOffsets() {
|
|
134
|
+
for (const filePath of this.collectFiles(this.watchPath)) {
|
|
135
|
+
try {
|
|
136
|
+
const stats = fs.statSync(filePath);
|
|
137
|
+
if (stats.isFile()) {
|
|
138
|
+
this.fileOffsets.set(filePath, stats.size);
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
collectFiles(targetPath) {
|
|
145
|
+
if (!fs.existsSync(targetPath)) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
const resolved = path.resolve(targetPath);
|
|
149
|
+
const stats = fs.statSync(resolved);
|
|
150
|
+
if (stats.isFile()) {
|
|
151
|
+
return [resolved];
|
|
152
|
+
}
|
|
153
|
+
if (!stats.isDirectory()) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
const collected = [];
|
|
157
|
+
for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
|
|
158
|
+
const childPath = path.join(resolved, entry.name);
|
|
159
|
+
if (entry.isDirectory()) {
|
|
160
|
+
collected.push(...this.collectFiles(childPath));
|
|
161
|
+
} else if (entry.isFile()) {
|
|
162
|
+
collected.push(path.resolve(childPath));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return collected;
|
|
112
166
|
}
|
|
113
167
|
};
|
|
114
168
|
|
|
@@ -1,8 +1,9 @@
|
|
|
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(?:decision|decided|chose|chosen|selected|picked|opted|switched to)\s*:?|\bdecid(?:e|ed|ing|ion)\b|\berror\b|\bfail(?:ed|ure)?\b|\
|
|
5
|
-
var
|
|
4
|
+
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)/i;
|
|
5
|
+
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
|
+
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)\b/i;
|
|
6
7
|
var Compressor = class {
|
|
7
8
|
model;
|
|
8
9
|
now;
|
|
@@ -53,10 +54,12 @@ var Compressor = class {
|
|
|
53
54
|
"- Group observations by date heading: ## YYYY-MM-DD",
|
|
54
55
|
"- Each line must follow: <emoji> <HH:MM> <observation>",
|
|
55
56
|
"- Priority emojis: \u{1F534} critical, \u{1F7E1} notable, \u{1F7E2} info",
|
|
56
|
-
"- \u{1F534}
|
|
57
|
-
"- \u{1F7E1} for: architecture discussions, trade-offs, milestones,
|
|
57
|
+
"- \u{1F534} for: decisions between alternatives, errors/failures, blockers, deadlines with explicit dates, breaking changes",
|
|
58
|
+
"- \u{1F7E1} for: preferences, architecture discussions, trade-offs, milestones, people interactions, notable context, routine deadlines",
|
|
58
59
|
"- \u{1F7E2} for: routine updates, deployments, builds, general info",
|
|
59
|
-
"-
|
|
60
|
+
"- Preferences are \u{1F7E1} unless they indicate a breaking decision between alternatives.",
|
|
61
|
+
"- Each distinct error type or failure must be its own observation line; do not merge different errors.",
|
|
62
|
+
"- If multiple different errors occurred, list each separately with its specific error message.",
|
|
60
63
|
"- Keep observations concise and factual.",
|
|
61
64
|
"- Avoid duplicates when possible.",
|
|
62
65
|
"",
|
|
@@ -161,7 +164,7 @@ ${cleaned}`;
|
|
|
161
164
|
}
|
|
162
165
|
/**
|
|
163
166
|
* Post-process LLM output to enforce priority rules.
|
|
164
|
-
* Lines matching
|
|
167
|
+
* Lines matching critical rules get upgraded to 🔴, notable rules to 🟡.
|
|
165
168
|
*/
|
|
166
169
|
enforcePriorityRules(markdown) {
|
|
167
170
|
return markdown.split(/\r?\n/).map((line) => {
|
|
@@ -169,10 +172,10 @@ ${cleaned}`;
|
|
|
169
172
|
if (!match) return line;
|
|
170
173
|
const currentPriority = match[1];
|
|
171
174
|
const content = match[2];
|
|
172
|
-
if (
|
|
175
|
+
if (this.isCriticalContent(content) && currentPriority !== "\u{1F534}") {
|
|
173
176
|
return line.replace(/^🟡|^🟢/u, "\u{1F534}");
|
|
174
177
|
}
|
|
175
|
-
if (
|
|
178
|
+
if (this.isNotableContent(content) && currentPriority === "\u{1F7E2}") {
|
|
176
179
|
return line.replace(/^🟢/u, "\u{1F7E1}");
|
|
177
180
|
}
|
|
178
181
|
return line;
|
|
@@ -284,10 +287,16 @@ ${cleaned}`;
|
|
|
284
287
|
return chunks.join("\n").trim();
|
|
285
288
|
}
|
|
286
289
|
inferPriority(text) {
|
|
287
|
-
if (
|
|
288
|
-
if (
|
|
290
|
+
if (this.isCriticalContent(text)) return "\u{1F534}";
|
|
291
|
+
if (this.isNotableContent(text)) return "\u{1F7E1}";
|
|
289
292
|
return "\u{1F7E2}";
|
|
290
293
|
}
|
|
294
|
+
isCriticalContent(text) {
|
|
295
|
+
return CRITICAL_RE.test(text) || DEADLINE_WITH_DATE_RE.test(text);
|
|
296
|
+
}
|
|
297
|
+
isNotableContent(text) {
|
|
298
|
+
return NOTABLE_RE.test(text);
|
|
299
|
+
}
|
|
291
300
|
normalizeText(text) {
|
|
292
301
|
return text.replace(/\s+/g, " ").replace(/\[[^\]]+\]/g, "").trim().slice(0, 280);
|
|
293
302
|
}
|
|
@@ -58,11 +58,71 @@ 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
|
+
}
|
|
61
88
|
|
|
62
89
|
// src/commands/context.ts
|
|
63
90
|
var DEFAULT_LIMIT = 5;
|
|
64
91
|
var MAX_SNIPPET_LENGTH = 320;
|
|
65
92
|
var OBSERVATION_LOOKBACK_DAYS = 7;
|
|
93
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
94
|
+
"a",
|
|
95
|
+
"an",
|
|
96
|
+
"and",
|
|
97
|
+
"are",
|
|
98
|
+
"as",
|
|
99
|
+
"at",
|
|
100
|
+
"be",
|
|
101
|
+
"by",
|
|
102
|
+
"for",
|
|
103
|
+
"from",
|
|
104
|
+
"how",
|
|
105
|
+
"in",
|
|
106
|
+
"is",
|
|
107
|
+
"it",
|
|
108
|
+
"of",
|
|
109
|
+
"on",
|
|
110
|
+
"or",
|
|
111
|
+
"that",
|
|
112
|
+
"the",
|
|
113
|
+
"this",
|
|
114
|
+
"to",
|
|
115
|
+
"was",
|
|
116
|
+
"were",
|
|
117
|
+
"what",
|
|
118
|
+
"when",
|
|
119
|
+
"where",
|
|
120
|
+
"who",
|
|
121
|
+
"why",
|
|
122
|
+
"with",
|
|
123
|
+
"you",
|
|
124
|
+
"your"
|
|
125
|
+
]);
|
|
66
126
|
function formatRelativeAge(date, now = Date.now()) {
|
|
67
127
|
const ageMs = Math.max(0, now - date.getTime());
|
|
68
128
|
const days = Math.floor(ageMs / (24 * 60 * 60 * 1e3));
|
|
@@ -97,6 +157,35 @@ function formatContextMarkdown(task, entries) {
|
|
|
97
157
|
}
|
|
98
158
|
return output.trimEnd();
|
|
99
159
|
}
|
|
160
|
+
function extractKeywords(text) {
|
|
161
|
+
const raw = text.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
162
|
+
const seen = /* @__PURE__ */ new Set();
|
|
163
|
+
const keywords = [];
|
|
164
|
+
for (const token of raw) {
|
|
165
|
+
if (token.length < 2 || STOP_WORDS.has(token) || seen.has(token)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
seen.add(token);
|
|
169
|
+
keywords.push(token);
|
|
170
|
+
}
|
|
171
|
+
return keywords;
|
|
172
|
+
}
|
|
173
|
+
function computeKeywordOverlapScore(queryKeywords, text) {
|
|
174
|
+
if (queryKeywords.length === 0) {
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
const haystack = new Set(extractKeywords(text));
|
|
178
|
+
let matches = 0;
|
|
179
|
+
for (const keyword of queryKeywords) {
|
|
180
|
+
if (haystack.has(keyword)) {
|
|
181
|
+
matches += 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (matches === 0) {
|
|
185
|
+
return 0.1;
|
|
186
|
+
}
|
|
187
|
+
return matches / queryKeywords.length;
|
|
188
|
+
}
|
|
100
189
|
function estimateSnippet(source) {
|
|
101
190
|
const normalized = source.replace(/\s+/g, " ").trim();
|
|
102
191
|
if (!normalized) {
|
|
@@ -134,11 +223,6 @@ function observationPriorityToRank(priority) {
|
|
|
134
223
|
if (priority === "\u{1F7E1}") return 4;
|
|
135
224
|
return 5;
|
|
136
225
|
}
|
|
137
|
-
function observationPriorityScore(priority) {
|
|
138
|
-
if (priority === "\u{1F534}") return 1;
|
|
139
|
-
if (priority === "\u{1F7E1}") return 0.7;
|
|
140
|
-
return 0.4;
|
|
141
|
-
}
|
|
142
226
|
function isLikelyDailyNote(document) {
|
|
143
227
|
const normalizedPath = document.path.split(path2.sep).join("/").toLowerCase();
|
|
144
228
|
if (normalizedPath.includes("/daily/")) {
|
|
@@ -212,7 +296,7 @@ async function buildDailyContextItems(vault) {
|
|
|
212
296
|
}
|
|
213
297
|
return items;
|
|
214
298
|
}
|
|
215
|
-
function buildObservationContextItems(vaultPath) {
|
|
299
|
+
function buildObservationContextItems(vaultPath, queryKeywords) {
|
|
216
300
|
const observationMarkdown = readObservations(vaultPath, OBSERVATION_LOOKBACK_DAYS);
|
|
217
301
|
const parsed = parseObservationLines(observationMarkdown);
|
|
218
302
|
const items = [];
|
|
@@ -227,7 +311,7 @@ function buildObservationContextItems(vaultPath) {
|
|
|
227
311
|
title: `${observation.priority} observation (${date})`,
|
|
228
312
|
path: `observations/${date}.md`,
|
|
229
313
|
category: "observations",
|
|
230
|
-
score:
|
|
314
|
+
score: computeKeywordOverlapScore(queryKeywords, observation.content),
|
|
231
315
|
snippet,
|
|
232
316
|
modified: modifiedDate.toISOString(),
|
|
233
317
|
age: formatRelativeAge(modifiedDate),
|
|
@@ -279,21 +363,31 @@ function applyTokenBudget(items, task, budget) {
|
|
|
279
363
|
return { context: fullContext, markdown: fullMarkdown };
|
|
280
364
|
}
|
|
281
365
|
const normalizedBudget = Math.max(1, Math.floor(budget));
|
|
366
|
+
if (estimateTokens(fullMarkdown) <= normalizedBudget) {
|
|
367
|
+
return { context: fullContext, markdown: fullMarkdown };
|
|
368
|
+
}
|
|
282
369
|
const header = `## Relevant Context for: ${task}
|
|
283
370
|
|
|
284
371
|
`;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
const cost = estimateTokens(renderEntryBlock(item.entry));
|
|
292
|
-
if (cost <= remaining) {
|
|
293
|
-
selectedEntries.push(item.entry);
|
|
294
|
-
remaining -= cost;
|
|
295
|
-
}
|
|
372
|
+
const headerCost = estimateTokens(header);
|
|
373
|
+
if (headerCost >= normalizedBudget) {
|
|
374
|
+
return {
|
|
375
|
+
context: [],
|
|
376
|
+
markdown: truncateToBudget(header.trimEnd(), normalizedBudget)
|
|
377
|
+
};
|
|
296
378
|
}
|
|
379
|
+
const fitted = fitWithinBudget(
|
|
380
|
+
items.map((item, index) => ({
|
|
381
|
+
text: renderEntryBlock(item.entry),
|
|
382
|
+
priority: item.priority,
|
|
383
|
+
source: String(index)
|
|
384
|
+
})),
|
|
385
|
+
normalizedBudget - headerCost
|
|
386
|
+
);
|
|
387
|
+
const selectedEntries = fitted.map((item) => {
|
|
388
|
+
const index = Number.parseInt(item.source, 10);
|
|
389
|
+
return Number.isNaN(index) ? null : items[index]?.entry ?? null;
|
|
390
|
+
}).filter((entry) => Boolean(entry));
|
|
297
391
|
const markdown = truncateToBudget(formatContextMarkdown(task, selectedEntries), normalizedBudget);
|
|
298
392
|
return {
|
|
299
393
|
context: selectedEntries,
|
|
@@ -310,20 +404,24 @@ async function buildContext(task, options) {
|
|
|
310
404
|
const limit = Math.max(1, options.limit ?? DEFAULT_LIMIT);
|
|
311
405
|
const recent = options.recent ?? true;
|
|
312
406
|
const includeObservations = options.includeObservations ?? true;
|
|
407
|
+
const queryKeywords = extractKeywords(normalizedTask);
|
|
313
408
|
const searchResults = await vault.vsearch(normalizedTask, {
|
|
314
409
|
limit,
|
|
315
410
|
temporalBoost: recent
|
|
316
411
|
});
|
|
317
412
|
const searchItems = buildSearchContextItems(vault, searchResults);
|
|
318
413
|
const dailyItems = await buildDailyContextItems(vault);
|
|
319
|
-
const observationItems = includeObservations ? buildObservationContextItems(vault.getPath()) : [];
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
const
|
|
414
|
+
const observationItems = includeObservations ? buildObservationContextItems(vault.getPath(), queryKeywords) : [];
|
|
415
|
+
const byScoreDesc = (left, right) => right.entry.score - left.entry.score;
|
|
416
|
+
const redObservations = observationItems.filter((item) => item.priority === 1).sort(byScoreDesc);
|
|
417
|
+
const yellowObservations = observationItems.filter((item) => item.priority === 4).sort(byScoreDesc);
|
|
418
|
+
const greenObservations = observationItems.filter((item) => item.priority === 5).sort(byScoreDesc);
|
|
419
|
+
const sortedDailyItems = [...dailyItems].sort(byScoreDesc);
|
|
420
|
+
const sortedSearchItems = [...searchItems].sort(byScoreDesc);
|
|
323
421
|
const ordered = [
|
|
324
422
|
...redObservations,
|
|
325
|
-
...
|
|
326
|
-
...
|
|
423
|
+
...sortedDailyItems,
|
|
424
|
+
...sortedSearchItems,
|
|
327
425
|
...yellowObservations,
|
|
328
426
|
...greenObservations
|
|
329
427
|
];
|
package/dist/commands/context.js
CHANGED
package/dist/commands/observe.js
CHANGED
package/dist/commands/sleep.js
CHANGED
package/dist/commands/wake.js
CHANGED
|
@@ -15,6 +15,9 @@ import * as fs from "fs";
|
|
|
15
15
|
import * as path from "path";
|
|
16
16
|
var DEFAULT_HANDOFF_LIMIT = 3;
|
|
17
17
|
var OBSERVATION_HIGHLIGHT_RE = /^(🔴|🟡)\s+(.+)$/u;
|
|
18
|
+
var MAX_WAKE_RED_OBSERVATIONS = 20;
|
|
19
|
+
var MAX_WAKE_YELLOW_OBSERVATIONS = 10;
|
|
20
|
+
var MAX_WAKE_OUTPUT_LINES = 100;
|
|
18
21
|
function formatSummaryItems(items, maxItems = 2) {
|
|
19
22
|
const cleaned = items.map((item) => item.trim()).filter(Boolean);
|
|
20
23
|
if (cleaned.length === 0) return "";
|
|
@@ -60,23 +63,53 @@ function readRecentObservationHighlights(vaultPath) {
|
|
|
60
63
|
}
|
|
61
64
|
return highlights;
|
|
62
65
|
}
|
|
66
|
+
function timeFromObservationText(text) {
|
|
67
|
+
const match = text.match(/^([01]\d|2[0-3]):([0-5]\d)\b/);
|
|
68
|
+
if (!match) {
|
|
69
|
+
return -1;
|
|
70
|
+
}
|
|
71
|
+
return Number.parseInt(match[1], 10) * 60 + Number.parseInt(match[2], 10);
|
|
72
|
+
}
|
|
73
|
+
function compareByRecency(left, right) {
|
|
74
|
+
if (left.date !== right.date) {
|
|
75
|
+
return right.date.localeCompare(left.date);
|
|
76
|
+
}
|
|
77
|
+
return timeFromObservationText(right.text) - timeFromObservationText(left.text);
|
|
78
|
+
}
|
|
63
79
|
function formatRecentObservations(highlights) {
|
|
64
80
|
if (highlights.length === 0) {
|
|
65
81
|
return "_No critical or notable observations from today or yesterday._";
|
|
66
82
|
}
|
|
83
|
+
const sorted = [...highlights].sort(compareByRecency);
|
|
84
|
+
const red = sorted.filter((item) => item.priority === "\u{1F534}").slice(0, MAX_WAKE_RED_OBSERVATIONS);
|
|
85
|
+
const yellow = sorted.filter((item) => item.priority === "\u{1F7E1}").slice(0, MAX_WAKE_YELLOW_OBSERVATIONS);
|
|
86
|
+
const visible = [...red, ...yellow].sort(compareByRecency);
|
|
87
|
+
const omittedCount = Math.max(0, highlights.length - visible.length);
|
|
67
88
|
const byDate = /* @__PURE__ */ new Map();
|
|
68
|
-
for (const item of
|
|
89
|
+
for (const item of visible) {
|
|
69
90
|
const bucket = byDate.get(item.date) ?? [];
|
|
70
91
|
bucket.push(item);
|
|
71
92
|
byDate.set(item.date, bucket);
|
|
72
93
|
}
|
|
73
94
|
const lines = [];
|
|
95
|
+
const bodyLineBudget = Math.max(1, MAX_WAKE_OUTPUT_LINES - (omittedCount > 0 ? 1 : 0));
|
|
74
96
|
for (const [date, items] of byDate.entries()) {
|
|
97
|
+
if (lines.length >= bodyLineBudget) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
75
100
|
lines.push(`### ${date}`);
|
|
76
101
|
for (const item of items) {
|
|
102
|
+
if (lines.length >= bodyLineBudget) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
77
105
|
lines.push(`- ${item.priority} ${item.text}`);
|
|
78
106
|
}
|
|
79
|
-
lines.
|
|
107
|
+
if (lines.length < bodyLineBudget) {
|
|
108
|
+
lines.push("");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (omittedCount > 0) {
|
|
112
|
+
lines.push(`... and ${omittedCount} more observations (use \`clawvault context\` to query)`);
|
|
80
113
|
}
|
|
81
114
|
return lines.join("\n").trim();
|
|
82
115
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -333,7 +333,7 @@ declare class Compressor {
|
|
|
333
333
|
private normalizeLlmOutput;
|
|
334
334
|
/**
|
|
335
335
|
* Post-process LLM output to enforce priority rules.
|
|
336
|
-
* Lines matching
|
|
336
|
+
* Lines matching critical rules get upgraded to 🔴, notable rules to 🟡.
|
|
337
337
|
*/
|
|
338
338
|
private enforcePriorityRules;
|
|
339
339
|
private fallbackCompression;
|
|
@@ -343,6 +343,8 @@ declare class Compressor {
|
|
|
343
343
|
private parseSections;
|
|
344
344
|
private renderSections;
|
|
345
345
|
private inferPriority;
|
|
346
|
+
private isCriticalContent;
|
|
347
|
+
private isNotableContent;
|
|
346
348
|
private normalizeText;
|
|
347
349
|
private extractDate;
|
|
348
350
|
private extractTime;
|
|
@@ -368,22 +370,28 @@ declare class Reflector {
|
|
|
368
370
|
interface SessionWatcherOptions {
|
|
369
371
|
ignoreInitial?: boolean;
|
|
370
372
|
debounceMs?: number;
|
|
373
|
+
flushThresholdChars?: number;
|
|
371
374
|
}
|
|
372
375
|
declare class SessionWatcher {
|
|
373
376
|
private readonly watchPath;
|
|
374
377
|
private readonly observer;
|
|
375
378
|
private readonly ignoreInitial;
|
|
376
379
|
private readonly debounceMs;
|
|
380
|
+
private readonly flushThresholdChars;
|
|
377
381
|
private watcher;
|
|
378
382
|
private fileOffsets;
|
|
379
383
|
private pendingPaths;
|
|
380
384
|
private debounceTimer;
|
|
381
385
|
private processingQueue;
|
|
386
|
+
private bufferedChars;
|
|
382
387
|
constructor(watchPath: string, observer: Observer, options?: SessionWatcherOptions);
|
|
383
388
|
start(): Promise<void>;
|
|
384
389
|
stop(): Promise<void>;
|
|
385
390
|
private scheduleDrain;
|
|
391
|
+
private drainPendingPaths;
|
|
386
392
|
private consumeFile;
|
|
393
|
+
private primeInitialOffsets;
|
|
394
|
+
private collectFiles;
|
|
387
395
|
}
|
|
388
396
|
|
|
389
397
|
declare function parseSessionFile(filePath: string): string[];
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
contextCommand,
|
|
17
17
|
formatContextMarkdown,
|
|
18
18
|
registerContextCommand
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-UBRYOIII.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-
|
|
44
|
+
} from "./chunk-2MP4EHJ7.js";
|
|
45
45
|
import {
|
|
46
46
|
Compressor,
|
|
47
47
|
Observer,
|
|
48
48
|
Reflector,
|
|
49
49
|
parseSessionFile
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-SIDM2I2C.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.
|
|
3
|
+
"version": "1.10.0",
|
|
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",
|
package/dashboard/test-crash.mjs
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import puppeteer from 'puppeteer';
|
|
2
|
-
|
|
3
|
-
async function test() {
|
|
4
|
-
console.log('Launching browser...');
|
|
5
|
-
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });
|
|
6
|
-
const page = await browser.newPage();
|
|
7
|
-
|
|
8
|
-
const errors = [];
|
|
9
|
-
page.on('console', msg => {
|
|
10
|
-
if (msg.type() === 'error') errors.push(msg.text());
|
|
11
|
-
});
|
|
12
|
-
page.on('pageerror', err => errors.push(err.message));
|
|
13
|
-
|
|
14
|
-
console.log('Loading dashboard...');
|
|
15
|
-
try {
|
|
16
|
-
await page.goto('http://localhost:3377', { timeout: 30000, waitUntil: 'networkidle0' });
|
|
17
|
-
console.log('Page loaded, waiting 10s...');
|
|
18
|
-
await new Promise(r => setTimeout(r, 10000));
|
|
19
|
-
|
|
20
|
-
const nodeCount = await page.evaluate(() => {
|
|
21
|
-
const stats = document.querySelector('#stats')?.textContent || '';
|
|
22
|
-
return stats;
|
|
23
|
-
});
|
|
24
|
-
console.log('Stats:', nodeCount);
|
|
25
|
-
console.log('Errors:', errors.length ? errors : 'none');
|
|
26
|
-
|
|
27
|
-
await page.screenshot({ path: 'dashboard/test-screenshot.png' });
|
|
28
|
-
console.log('Screenshot saved to dashboard/test-screenshot.png');
|
|
29
|
-
} catch (e) {
|
|
30
|
-
console.log('CRASH:', e.message);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
await browser.close();
|
|
34
|
-
console.log('Test complete');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
test();
|
|
Binary file
|