clawvault 2.0.2 β 2.1.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 +5 -0
- package/bin/register-query-commands.js +16 -0
- package/dist/{chunk-NU7VKTWG.js β chunk-MVX4NNVJ.js} +2 -1
- package/dist/chunk-YHKGPRII.js +723 -0
- package/dist/commands/observe.d.ts +5 -0
- package/dist/commands/observe.js +4 -2
- package/dist/commands/sleep.js +1 -1
- package/dist/index.js +2 -2
- package/hooks/clawvault/HOOK.md +4 -2
- package/hooks/clawvault/handler.js +238 -2
- package/hooks/clawvault/handler.test.js +82 -0
- package/package.json +1 -1
- package/dist/chunk-BAS32WLF.js +0 -319
|
@@ -8,6 +8,11 @@ interface ObserveCommandOptions {
|
|
|
8
8
|
compress?: string;
|
|
9
9
|
daemon?: boolean;
|
|
10
10
|
vaultPath?: string;
|
|
11
|
+
active?: boolean;
|
|
12
|
+
agent?: string;
|
|
13
|
+
minNew?: number;
|
|
14
|
+
sessionsDir?: string;
|
|
15
|
+
dryRun?: boolean;
|
|
11
16
|
}
|
|
12
17
|
declare function observeCommand(options: ObserveCommandOptions): Promise<void>;
|
|
13
18
|
declare function registerObserveCommand(program: Command): void;
|
package/dist/commands/observe.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
observeCommand,
|
|
3
3
|
registerObserveCommand
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-YHKGPRII.js";
|
|
5
|
+
import "../chunk-HRLWZGMA.js";
|
|
6
|
+
import "../chunk-MVX4NNVJ.js";
|
|
7
|
+
import "../chunk-MXSSG3QU.js";
|
|
6
8
|
export {
|
|
7
9
|
observeCommand,
|
|
8
10
|
registerObserveCommand
|
package/dist/commands/sleep.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
SessionWatcher,
|
|
7
7
|
observeCommand,
|
|
8
8
|
registerObserveCommand
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-YHKGPRII.js";
|
|
10
10
|
import {
|
|
11
11
|
buildSessionRecap,
|
|
12
12
|
formatSessionRecapMarkdown,
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
Observer,
|
|
22
22
|
Reflector,
|
|
23
23
|
parseSessionFile
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-MVX4NNVJ.js";
|
|
25
25
|
import {
|
|
26
26
|
buildContext,
|
|
27
27
|
contextCommand,
|
package/hooks/clawvault/HOOK.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Context resilience - recovery detection, auto-checkpoint, and sess
|
|
|
4
4
|
metadata:
|
|
5
5
|
openclaw:
|
|
6
6
|
emoji: "π"
|
|
7
|
-
events: ["gateway:startup", "command:new", "session:start"]
|
|
7
|
+
events: ["gateway:startup", "gateway:heartbeat", "command:new", "session:start", "compaction:memoryFlush"]
|
|
8
8
|
requires:
|
|
9
9
|
bins: ["clawvault"]
|
|
10
10
|
---
|
|
@@ -14,7 +14,9 @@ metadata:
|
|
|
14
14
|
Integrates ClawVault's context death resilience into OpenClaw:
|
|
15
15
|
|
|
16
16
|
- **On gateway startup**: Checks for context death, alerts agent
|
|
17
|
+
- **On heartbeat**: Runs cheap threshold checks and observes active sessions when needed
|
|
17
18
|
- **On /new command**: Auto-checkpoints before session reset
|
|
19
|
+
- **On context compaction**: Forces incremental observation flush before context is lost
|
|
18
20
|
- **On session start**: Injects relevant vault context for the initial prompt
|
|
19
21
|
|
|
20
22
|
## Installation
|
|
@@ -61,7 +63,7 @@ Injection format:
|
|
|
61
63
|
|
|
62
64
|
### Event Compatibility
|
|
63
65
|
|
|
64
|
-
The hook accepts canonical OpenClaw events (`gateway:startup`, `command:new`, `session:start`) and tolerates alias payload shapes (`event`, `eventName`, `name`, `hook`, `trigger`) to remain robust across runtime wrappers.
|
|
66
|
+
The hook accepts canonical OpenClaw events (`gateway:startup`, `gateway:heartbeat`, `command:new`, `session:start`, `compaction:memoryFlush`) and tolerates alias payload shapes (`event`, `eventName`, `name`, `hook`, `trigger`) to remain robust across runtime wrappers.
|
|
65
67
|
|
|
66
68
|
## No Configuration Needed
|
|
67
69
|
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides automatic context death resilience:
|
|
5
5
|
* - gateway:startup β detect context death, inject recovery info
|
|
6
|
+
* - gateway:heartbeat β cheap active-session threshold checks
|
|
6
7
|
* - command:new β auto-checkpoint before session reset
|
|
8
|
+
* - compaction:memoryFlush β force active-session flush before compaction
|
|
7
9
|
* - session:start β inject relevant context for first user prompt
|
|
8
10
|
*
|
|
9
11
|
* SECURITY: Uses execFileSync (no shell) to prevent command injection
|
|
@@ -11,6 +13,7 @@
|
|
|
11
13
|
|
|
12
14
|
import { execFileSync } from 'child_process';
|
|
13
15
|
import * as fs from 'fs';
|
|
16
|
+
import * as os from 'os';
|
|
14
17
|
import * as path from 'path';
|
|
15
18
|
|
|
16
19
|
const MAX_CONTEXT_RESULTS = 4;
|
|
@@ -19,6 +22,12 @@ const MAX_CONTEXT_SNIPPET_LENGTH = 220;
|
|
|
19
22
|
const MAX_RECAP_RESULTS = 6;
|
|
20
23
|
const MAX_RECAP_SNIPPET_LENGTH = 220;
|
|
21
24
|
const EVENT_NAME_SEPARATOR_RE = /[.:/]/g;
|
|
25
|
+
const OBSERVE_CURSOR_FILE = 'observe-cursors.json';
|
|
26
|
+
const ONE_KIB = 1024;
|
|
27
|
+
const ONE_MIB = ONE_KIB * ONE_KIB;
|
|
28
|
+
const SMALL_SESSION_THRESHOLD_BYTES = 50 * ONE_KIB;
|
|
29
|
+
const MEDIUM_SESSION_THRESHOLD_BYTES = 150 * ONE_KIB;
|
|
30
|
+
const LARGE_SESSION_THRESHOLD_BYTES = 300 * ONE_KIB;
|
|
22
31
|
|
|
23
32
|
// Sanitize string for safe display (prevent prompt injection via control chars)
|
|
24
33
|
function sanitizeForDisplay(str) {
|
|
@@ -74,6 +83,128 @@ function extractAgentIdFromSessionKey(sessionKey) {
|
|
|
74
83
|
return agentId;
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
function sanitizeAgentId(agentId) {
|
|
87
|
+
if (typeof agentId !== 'string') return '';
|
|
88
|
+
const normalized = agentId.trim();
|
|
89
|
+
if (!/^[a-zA-Z0-9_-]{1,100}$/.test(normalized)) return '';
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeAbsoluteEnvPath(value) {
|
|
94
|
+
if (typeof value !== 'string') return null;
|
|
95
|
+
const trimmed = value.trim();
|
|
96
|
+
if (!trimmed) return null;
|
|
97
|
+
const resolved = path.resolve(trimmed);
|
|
98
|
+
if (!path.isAbsolute(resolved)) return null;
|
|
99
|
+
return resolved;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getOpenClawAgentsDir() {
|
|
103
|
+
const stateDir = normalizeAbsoluteEnvPath(process.env.OPENCLAW_STATE_DIR);
|
|
104
|
+
if (stateDir) {
|
|
105
|
+
return path.join(stateDir, 'agents');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const openClawHome = normalizeAbsoluteEnvPath(process.env.OPENCLAW_HOME);
|
|
109
|
+
if (openClawHome) {
|
|
110
|
+
return path.join(openClawHome, 'agents');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return path.join(os.homedir(), '.openclaw', 'agents');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getObserveCursorPath(vaultPath) {
|
|
117
|
+
return path.join(vaultPath, '.clawvault', OBSERVE_CURSOR_FILE);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function loadObserveCursors(vaultPath) {
|
|
121
|
+
const cursorPath = getObserveCursorPath(vaultPath);
|
|
122
|
+
if (!fs.existsSync(cursorPath)) {
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(fs.readFileSync(cursorPath, 'utf-8'));
|
|
128
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
return parsed;
|
|
132
|
+
} catch {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getScaledObservationThresholdBytes(fileSizeBytes) {
|
|
138
|
+
if (!Number.isFinite(fileSizeBytes) || fileSizeBytes <= 0) {
|
|
139
|
+
return SMALL_SESSION_THRESHOLD_BYTES;
|
|
140
|
+
}
|
|
141
|
+
if (fileSizeBytes < ONE_MIB) {
|
|
142
|
+
return SMALL_SESSION_THRESHOLD_BYTES;
|
|
143
|
+
}
|
|
144
|
+
if (fileSizeBytes <= 5 * ONE_MIB) {
|
|
145
|
+
return MEDIUM_SESSION_THRESHOLD_BYTES;
|
|
146
|
+
}
|
|
147
|
+
return LARGE_SESSION_THRESHOLD_BYTES;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseSessionIndex(agentId) {
|
|
151
|
+
const sessionsDir = path.join(getOpenClawAgentsDir(), agentId, 'sessions');
|
|
152
|
+
const sessionsJsonPath = path.join(sessionsDir, 'sessions.json');
|
|
153
|
+
if (!fs.existsSync(sessionsJsonPath)) {
|
|
154
|
+
return { sessionsDir, index: {} };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(fs.readFileSync(sessionsJsonPath, 'utf-8'));
|
|
159
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
160
|
+
return { sessionsDir, index: {} };
|
|
161
|
+
}
|
|
162
|
+
return { sessionsDir, index: parsed };
|
|
163
|
+
} catch {
|
|
164
|
+
return { sessionsDir, index: {} };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function shouldObserveActiveSessions(vaultPath, agentId) {
|
|
169
|
+
const cursors = loadObserveCursors(vaultPath);
|
|
170
|
+
const { sessionsDir, index } = parseSessionIndex(agentId);
|
|
171
|
+
const entries = Object.entries(index);
|
|
172
|
+
if (entries.length === 0) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const [sessionKey, value] of entries) {
|
|
177
|
+
if (!value || typeof value !== 'object') continue;
|
|
178
|
+
const sessionId = typeof value.sessionId === 'string' ? value.sessionId.trim() : '';
|
|
179
|
+
if (!/^[a-zA-Z0-9._-]{1,200}$/.test(sessionId)) continue;
|
|
180
|
+
|
|
181
|
+
const filePath = path.join(sessionsDir, `${sessionId}.jsonl`);
|
|
182
|
+
let stat;
|
|
183
|
+
try {
|
|
184
|
+
stat = fs.statSync(filePath);
|
|
185
|
+
} catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!stat.isFile()) continue;
|
|
189
|
+
|
|
190
|
+
const fileSize = stat.size;
|
|
191
|
+
const cursorEntry = cursors[sessionId];
|
|
192
|
+
const previousOffset = Number.isFinite(cursorEntry?.lastObservedOffset)
|
|
193
|
+
? Math.max(0, Number(cursorEntry.lastObservedOffset))
|
|
194
|
+
: 0;
|
|
195
|
+
const startOffset = previousOffset <= fileSize ? previousOffset : 0;
|
|
196
|
+
const newBytes = Math.max(0, fileSize - startOffset);
|
|
197
|
+
const thresholdBytes = getScaledObservationThresholdBytes(fileSize);
|
|
198
|
+
|
|
199
|
+
if (newBytes >= thresholdBytes) {
|
|
200
|
+
console.log(`[clawvault] Active observe trigger: ${sessionKey} (+${newBytes}B >= ${thresholdBytes}B)`);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
77
208
|
function extractTextFromMessage(message) {
|
|
78
209
|
if (typeof message === 'string') return message;
|
|
79
210
|
if (!message || typeof message !== 'object') return '';
|
|
@@ -268,6 +399,26 @@ function eventMatches(event, type, action) {
|
|
|
268
399
|
return false;
|
|
269
400
|
}
|
|
270
401
|
|
|
402
|
+
function eventIncludesToken(event, token) {
|
|
403
|
+
const normalizedToken = normalizeEventToken(token);
|
|
404
|
+
if (!normalizedToken) return false;
|
|
405
|
+
|
|
406
|
+
const values = [
|
|
407
|
+
event?.type,
|
|
408
|
+
event?.action,
|
|
409
|
+
event?.event,
|
|
410
|
+
event?.name,
|
|
411
|
+
event?.hook,
|
|
412
|
+
event?.trigger,
|
|
413
|
+
event?.eventName
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
return values
|
|
417
|
+
.map((value) => normalizeEventToken(value))
|
|
418
|
+
.filter(Boolean)
|
|
419
|
+
.some((value) => value.includes(normalizedToken));
|
|
420
|
+
}
|
|
421
|
+
|
|
271
422
|
// Validate vault path - must be absolute and exist
|
|
272
423
|
function validateVaultPath(vaultPath) {
|
|
273
424
|
if (!vaultPath || typeof vaultPath !== 'string') return null;
|
|
@@ -320,13 +471,14 @@ function findVaultPath() {
|
|
|
320
471
|
}
|
|
321
472
|
|
|
322
473
|
// Run clawvault command safely (no shell)
|
|
323
|
-
function runClawvault(args) {
|
|
474
|
+
function runClawvault(args, options = {}) {
|
|
475
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(1000, Number(options.timeoutMs)) : 15000;
|
|
324
476
|
try {
|
|
325
477
|
// Use execFileSync to avoid shell injection
|
|
326
478
|
// Arguments are passed as array, not interpolated into shell
|
|
327
479
|
const output = execFileSync('clawvault', args, {
|
|
328
480
|
encoding: 'utf-8',
|
|
329
|
-
timeout:
|
|
481
|
+
timeout: timeoutMs,
|
|
330
482
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
331
483
|
// Explicitly no shell
|
|
332
484
|
shell: false
|
|
@@ -366,6 +518,32 @@ function parseRecoveryOutput(output) {
|
|
|
366
518
|
return { hadDeath, workingOn };
|
|
367
519
|
}
|
|
368
520
|
|
|
521
|
+
function resolveAgentIdForEvent(event) {
|
|
522
|
+
const fromSessionKey = extractAgentIdFromSessionKey(extractSessionKey(event));
|
|
523
|
+
if (fromSessionKey) return fromSessionKey;
|
|
524
|
+
|
|
525
|
+
const fromEnv = sanitizeAgentId(process.env.OPENCLAW_AGENT_ID);
|
|
526
|
+
if (fromEnv) return fromEnv;
|
|
527
|
+
|
|
528
|
+
return 'clawdious';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function runActiveObservation(vaultPath, agentId, options = {}) {
|
|
532
|
+
const args = ['observe', '--active', '--agent', agentId, '-v', vaultPath];
|
|
533
|
+
if (Number.isFinite(options.minNewBytes) && Number(options.minNewBytes) > 0) {
|
|
534
|
+
args.push('--min-new', String(Math.floor(Number(options.minNewBytes))));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const result = runClawvault(args, { timeoutMs: 120000 });
|
|
538
|
+
if (!result.success) {
|
|
539
|
+
console.warn(`[clawvault] Active observation failed (${options.reason || 'unknown reason'})`);
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
console.log(`[clawvault] Active observation complete (${options.reason || 'triggered'})`);
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
|
|
369
547
|
// Handle gateway startup - check for context death
|
|
370
548
|
async function handleStartup(event) {
|
|
371
549
|
const vaultPath = findVaultPath();
|
|
@@ -436,6 +614,12 @@ async function handleNew(event) {
|
|
|
436
614
|
} else {
|
|
437
615
|
console.warn('[clawvault] Auto-checkpoint failed');
|
|
438
616
|
}
|
|
617
|
+
|
|
618
|
+
const agentId = resolveAgentIdForEvent(event);
|
|
619
|
+
runActiveObservation(vaultPath, agentId, {
|
|
620
|
+
minNewBytes: 1,
|
|
621
|
+
reason: 'command:new flush'
|
|
622
|
+
});
|
|
439
623
|
}
|
|
440
624
|
|
|
441
625
|
// Handle session start - inject dynamic context for first prompt
|
|
@@ -500,6 +684,38 @@ async function handleSessionStart(event) {
|
|
|
500
684
|
}
|
|
501
685
|
}
|
|
502
686
|
|
|
687
|
+
// Handle heartbeat events - cheap stat-based trigger for active observation
|
|
688
|
+
async function handleHeartbeat(event) {
|
|
689
|
+
const vaultPath = findVaultPath();
|
|
690
|
+
if (!vaultPath) {
|
|
691
|
+
console.log('[clawvault] No vault found, skipping heartbeat observation check');
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const agentId = resolveAgentIdForEvent(event);
|
|
696
|
+
if (!shouldObserveActiveSessions(vaultPath, agentId)) {
|
|
697
|
+
console.log('[clawvault] Heartbeat: no sessions crossed active-observe threshold');
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
runActiveObservation(vaultPath, agentId, { reason: 'heartbeat threshold crossed' });
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Handle context compaction - force flush any pending session deltas
|
|
705
|
+
async function handleContextCompaction(event) {
|
|
706
|
+
const vaultPath = findVaultPath();
|
|
707
|
+
if (!vaultPath) {
|
|
708
|
+
console.log('[clawvault] No vault found, skipping compaction observation');
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const agentId = resolveAgentIdForEvent(event);
|
|
713
|
+
runActiveObservation(vaultPath, agentId, {
|
|
714
|
+
minNewBytes: 1,
|
|
715
|
+
reason: 'context compaction'
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
503
719
|
// Main handler - route events
|
|
504
720
|
const handler = async (event) => {
|
|
505
721
|
try {
|
|
@@ -508,6 +724,26 @@ const handler = async (event) => {
|
|
|
508
724
|
return;
|
|
509
725
|
}
|
|
510
726
|
|
|
727
|
+
if (
|
|
728
|
+
eventMatches(event, 'gateway', 'heartbeat')
|
|
729
|
+
|| eventMatches(event, 'session', 'heartbeat')
|
|
730
|
+
|| eventIncludesToken(event, 'heartbeat')
|
|
731
|
+
) {
|
|
732
|
+
await handleHeartbeat(event);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (
|
|
737
|
+
eventMatches(event, 'compaction', 'memoryflush')
|
|
738
|
+
|| eventMatches(event, 'context', 'compaction')
|
|
739
|
+
|| eventMatches(event, 'context', 'compact')
|
|
740
|
+
|| eventIncludesToken(event, 'compaction')
|
|
741
|
+
|| eventIncludesToken(event, 'memoryflush')
|
|
742
|
+
) {
|
|
743
|
+
await handleContextCompaction(event);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
511
747
|
if (eventMatches(event, 'command', 'new')) {
|
|
512
748
|
await handleNew(event);
|
|
513
749
|
return;
|
|
@@ -17,6 +17,26 @@ function makeVaultFixture() {
|
|
|
17
17
|
return root;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function makeOpenClawSessionFixture(agentId, sessionId, transcriptBytes = 0) {
|
|
21
|
+
const stateRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'clawvault-openclaw-'));
|
|
22
|
+
const sessionsDir = path.join(stateRoot, 'agents', agentId, 'sessions');
|
|
23
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
24
|
+
fs.writeFileSync(
|
|
25
|
+
path.join(sessionsDir, 'sessions.json'),
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
[`agent:${agentId}:main`]: {
|
|
28
|
+
sessionId,
|
|
29
|
+
updatedAt: Date.now()
|
|
30
|
+
}
|
|
31
|
+
}),
|
|
32
|
+
'utf-8'
|
|
33
|
+
);
|
|
34
|
+
const transcriptPath = path.join(sessionsDir, `${sessionId}.jsonl`);
|
|
35
|
+
const payload = transcriptBytes > 0 ? 'x'.repeat(transcriptBytes) : '';
|
|
36
|
+
fs.writeFileSync(transcriptPath, payload, 'utf-8');
|
|
37
|
+
return { stateRoot, sessionsDir, transcriptPath };
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
async function loadHandler() {
|
|
21
41
|
vi.resetModules();
|
|
22
42
|
const mod = await import('./handler.js');
|
|
@@ -26,6 +46,9 @@ async function loadHandler() {
|
|
|
26
46
|
afterEach(() => {
|
|
27
47
|
vi.clearAllMocks();
|
|
28
48
|
delete process.env.CLAWVAULT_PATH;
|
|
49
|
+
delete process.env.OPENCLAW_STATE_DIR;
|
|
50
|
+
delete process.env.OPENCLAW_HOME;
|
|
51
|
+
delete process.env.OPENCLAW_AGENT_ID;
|
|
29
52
|
});
|
|
30
53
|
|
|
31
54
|
describe('clawvault hook handler', () => {
|
|
@@ -158,4 +181,63 @@ describe('clawvault hook handler', () => {
|
|
|
158
181
|
|
|
159
182
|
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
160
183
|
});
|
|
184
|
+
|
|
185
|
+
it('triggers active observation on heartbeat when threshold is crossed', async () => {
|
|
186
|
+
const vaultPath = makeVaultFixture();
|
|
187
|
+
const sessionId = 'heartbeat-session-1';
|
|
188
|
+
const openClawFixture = makeOpenClawSessionFixture('clawdious', sessionId, 70 * 1024);
|
|
189
|
+
process.env.CLAWVAULT_PATH = vaultPath;
|
|
190
|
+
process.env.OPENCLAW_STATE_DIR = openClawFixture.stateRoot;
|
|
191
|
+
|
|
192
|
+
fs.mkdirSync(path.join(vaultPath, '.clawvault'), { recursive: true });
|
|
193
|
+
fs.writeFileSync(
|
|
194
|
+
path.join(vaultPath, '.clawvault', 'observe-cursors.json'),
|
|
195
|
+
JSON.stringify({
|
|
196
|
+
[sessionId]: {
|
|
197
|
+
lastObservedOffset: 0,
|
|
198
|
+
lastObservedAt: '2026-02-14T00:00:00.000Z',
|
|
199
|
+
sessionKey: 'agent:clawdious:main',
|
|
200
|
+
lastFileSize: 0
|
|
201
|
+
}
|
|
202
|
+
}),
|
|
203
|
+
'utf-8'
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
execFileSyncMock.mockReturnValue('');
|
|
207
|
+
|
|
208
|
+
const handler = await loadHandler();
|
|
209
|
+
await handler({
|
|
210
|
+
type: 'gateway',
|
|
211
|
+
action: 'heartbeat'
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
215
|
+
'clawvault',
|
|
216
|
+
expect.arrayContaining(['observe', '--active', '--agent', 'clawdious']),
|
|
217
|
+
expect.objectContaining({ shell: false })
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
221
|
+
fs.rmSync(openClawFixture.stateRoot, { recursive: true, force: true });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('forces active observation flush on compaction events', async () => {
|
|
225
|
+
const vaultPath = makeVaultFixture();
|
|
226
|
+
process.env.CLAWVAULT_PATH = vaultPath;
|
|
227
|
+
execFileSyncMock.mockReturnValue('');
|
|
228
|
+
|
|
229
|
+
const handler = await loadHandler();
|
|
230
|
+
await handler({
|
|
231
|
+
eventName: 'compaction:memoryFlush',
|
|
232
|
+
sessionKey: 'agent:clawdious:main'
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
236
|
+
'clawvault',
|
|
237
|
+
expect.arrayContaining(['observe', '--active', '--min-new', '1']),
|
|
238
|
+
expect.objectContaining({ shell: false })
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
242
|
+
});
|
|
161
243
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawvault",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.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",
|