vektor-slipstream 1.2.3 → 1.3.1
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/axon.js +389 -0
- package/cerebellum.js +439 -0
- package/cortex.js +221 -0
- package/index.js +480 -0
- package/package.json +114 -94
- package/token.js +322 -0
package/axon.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cloak_axon.js
|
|
3
|
+
* Session boundary handler — fires at SessionStart and Stop.
|
|
4
|
+
* Writes one MemCell per session end. Loads last MemCell on session start.
|
|
5
|
+
*
|
|
6
|
+
* Implements EverMemOS-style Episodic Trace Formation:
|
|
7
|
+
* Stop → bundle session into a single MemCell → memory.remember()
|
|
8
|
+
* Start → memory.recall('[CLOAK_AXON_MEMCELL]') → seed context
|
|
9
|
+
*
|
|
10
|
+
* Architecture: CLOAK layer → Vektor semantic + temporal graphs
|
|
11
|
+
* Research: EverMemOS arXiv:2601.02163 §3.1 Episodic Trace Formation
|
|
12
|
+
* MAGMA arXiv:2601.03236 §3.3 dual-stream fast ingest
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Crash recovery — debounced write to .wolf/axon-recovery.json
|
|
23
|
+
// If process dies before Stop hook fires, this file survives.
|
|
24
|
+
// On next SessionStart we detect it, build a MemCell, write to Vektor, delete.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
let _crashWriteTimer = null;
|
|
27
|
+
const CRASH_WRITE_DEBOUNCE_MS = 2000; // write at most every 2s
|
|
28
|
+
|
|
29
|
+
function _getCrashLogPath(projectPath) {
|
|
30
|
+
const wolfDir = path.join(projectPath || process.cwd(), '.wolf');
|
|
31
|
+
return path.join(wolfDir, 'axon-recovery.json');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _writeCrashLog() {
|
|
35
|
+
if (!_sessionLog.sessionId || !_sessionLog.projectPath) return;
|
|
36
|
+
try {
|
|
37
|
+
const wolfDir = path.join(_sessionLog.projectPath, '.wolf');
|
|
38
|
+
if (!fs.existsSync(wolfDir)) fs.mkdirSync(wolfDir, { recursive: true });
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
_getCrashLogPath(_sessionLog.projectPath),
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
sessionId : _sessionLog.sessionId,
|
|
43
|
+
projectName : _sessionLog.projectName,
|
|
44
|
+
startTime : _sessionLog.startTime,
|
|
45
|
+
events : _sessionLog.events.slice(-20), // last 20 only
|
|
46
|
+
filesWritten : [..._sessionLog.filesWritten],
|
|
47
|
+
filesRead : [..._sessionLog.filesRead].slice(0, 10),
|
|
48
|
+
errorsEncountered: _sessionLog.errorsEncountered,
|
|
49
|
+
fixesApplied : _sessionLog.fixesApplied,
|
|
50
|
+
savedAt : Date.now(),
|
|
51
|
+
}, null, 2)
|
|
52
|
+
);
|
|
53
|
+
} catch {
|
|
54
|
+
// Never let crash log writes block or error — silent failure is fine
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _debouncedCrashWrite() {
|
|
59
|
+
// Hook mode: process is ephemeral — exits immediately after hook runs.
|
|
60
|
+
// A debounced setTimeout would never fire before process.exit(0) kills it.
|
|
61
|
+
// Write synchronously here instead (single small JSON file, acceptable).
|
|
62
|
+
//
|
|
63
|
+
// MCP server mode: process is long-lived. Debounce avoids excessive fs
|
|
64
|
+
// writes on busy sessions (many rapid logEvent calls compress to one write).
|
|
65
|
+
if (process.argv.includes('--hook')) {
|
|
66
|
+
_writeCrashLog();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (_crashWriteTimer) clearTimeout(_crashWriteTimer);
|
|
70
|
+
_crashWriteTimer = setTimeout(_writeCrashLog, CRASH_WRITE_DEBOUNCE_MS);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Session log — accumulated in RAM, never written to disk raw
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
const _sessionLog = {
|
|
77
|
+
sessionId : null,
|
|
78
|
+
projectPath : null,
|
|
79
|
+
projectName : null,
|
|
80
|
+
startTime : null,
|
|
81
|
+
events : [], // { type, content, ts }
|
|
82
|
+
filesWritten : new Set(),
|
|
83
|
+
filesRead : new Set(),
|
|
84
|
+
errorsEncountered: [],
|
|
85
|
+
fixesApplied : [],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function _resetLog() {
|
|
89
|
+
_sessionLog.sessionId = null;
|
|
90
|
+
_sessionLog.projectPath = null;
|
|
91
|
+
_sessionLog.projectName = null;
|
|
92
|
+
_sessionLog.startTime = null;
|
|
93
|
+
_sessionLog.events = [];
|
|
94
|
+
_sessionLog.filesWritten = new Set();
|
|
95
|
+
_sessionLog.filesRead = new Set();
|
|
96
|
+
_sessionLog.errorsEncountered = [];
|
|
97
|
+
_sessionLog.fixesApplied = [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// MemCell builder — condenses session into a single dense string
|
|
102
|
+
// EverMemOS calls this "Episodic Trace Formation" — converts chat/event
|
|
103
|
+
// stream into atomic facts + time-bounded summary.
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
function buildMemCell(log, tokenCount, narrative = '') {
|
|
106
|
+
const durationMs = Date.now() - log.startTime;
|
|
107
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
108
|
+
|
|
109
|
+
const parts = [
|
|
110
|
+
`[CLOAK_AXON_MEMCELL]`,
|
|
111
|
+
`session:${log.sessionId}`,
|
|
112
|
+
`project:${log.projectName}`,
|
|
113
|
+
`started:${new Date(log.startTime).toISOString()}`,
|
|
114
|
+
`ended:${new Date().toISOString()}`,
|
|
115
|
+
`duration:${durationMin}min`,
|
|
116
|
+
`tokens_used:~${tokenCount}`,
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
// Intent/goal — the "WHY" of the session, not just the "WHAT"
|
|
120
|
+
// This is what separates memory from telemetry.
|
|
121
|
+
if (narrative && narrative.trim()) {
|
|
122
|
+
parts.push(`narrative:${narrative.trim().slice(0, 400)}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (log.filesWritten.size > 0) {
|
|
126
|
+
parts.push(`files_written:${[...log.filesWritten].join(',')}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (log.filesRead.size > 0) {
|
|
130
|
+
parts.push(`files_read:${[...log.filesRead].slice(0, 10).join(',')}`); // cap at 10
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (log.errorsEncountered.length > 0) {
|
|
134
|
+
parts.push(`errors:${log.errorsEncountered.slice(0, 5).join(' | ')}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (log.fixesApplied.length > 0) {
|
|
138
|
+
parts.push(`fixes:${log.fixesApplied.slice(0, 5).join(' | ')}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Key events summary — most recent 5 only (not raw log dump)
|
|
142
|
+
if (log.events.length > 0) {
|
|
143
|
+
const recent = log.events.slice(-5).map(e => e.content).join(' | ');
|
|
144
|
+
parts.push(`key_events:${recent}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return parts.join(' || ');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Session ID generator
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
function generateSessionId() {
|
|
154
|
+
return `S-${Date.now().toString(36).toUpperCase()}-${Math.random().toString(36).slice(2,6).toUpperCase()}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Hook: SessionStart
|
|
159
|
+
// Loads the last MemCell from Vektor and returns it as seed context.
|
|
160
|
+
// Does NOT write anything to Vektor — read only.
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* onSessionStart({ projectPath, memory })
|
|
165
|
+
* Call this when Claude Code fires the SessionStart hook.
|
|
166
|
+
* Returns the last session's MemCell content as a string for context seeding.
|
|
167
|
+
*/
|
|
168
|
+
async function onSessionStart({ projectPath, memory } = {}) {
|
|
169
|
+
if (!projectPath) throw new Error('cloak_axon: projectPath is required');
|
|
170
|
+
if (!memory) throw new Error('cloak_axon: memory instance is required');
|
|
171
|
+
|
|
172
|
+
// ── Crash recovery check ─────────────────────────────────────────────────
|
|
173
|
+
// If a recovery file exists, the last session died without firing Stop.
|
|
174
|
+
// Rescue it: build a MemCell, write to Vektor, delete the file.
|
|
175
|
+
const crashLogPath = _getCrashLogPath(projectPath);
|
|
176
|
+
let crashRecovered = false;
|
|
177
|
+
try {
|
|
178
|
+
if (fs.existsSync(crashLogPath)) {
|
|
179
|
+
const crashed = JSON.parse(fs.readFileSync(crashLogPath, 'utf8'));
|
|
180
|
+
if (crashed.sessionId) {
|
|
181
|
+
// Reconstruct _sessionLog from crash data to build MemCell
|
|
182
|
+
const tempLog = {
|
|
183
|
+
sessionId : crashed.sessionId,
|
|
184
|
+
projectPath,
|
|
185
|
+
projectName : crashed.projectName,
|
|
186
|
+
startTime : crashed.startTime,
|
|
187
|
+
events : crashed.events || [],
|
|
188
|
+
filesWritten : new Set(crashed.filesWritten || []),
|
|
189
|
+
filesRead : new Set(crashed.filesRead || []),
|
|
190
|
+
errorsEncountered: crashed.errorsEncountered || [],
|
|
191
|
+
fixesApplied : crashed.fixesApplied || [],
|
|
192
|
+
};
|
|
193
|
+
const crashMemCell = buildMemCell(tempLog, 0) + ' || recovered_from_crash:true';
|
|
194
|
+
await memory.remember(crashMemCell).catch(() => {});
|
|
195
|
+
crashRecovered = true;
|
|
196
|
+
}
|
|
197
|
+
fs.unlinkSync(crashLogPath); // clean up regardless
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Recovery failure is non-fatal — proceed with new session
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Reset RAM log for new session
|
|
204
|
+
_resetLog();
|
|
205
|
+
_sessionLog.sessionId = generateSessionId();
|
|
206
|
+
_sessionLog.projectPath = projectPath;
|
|
207
|
+
_sessionLog.projectName = path.basename(projectPath);
|
|
208
|
+
_sessionLog.startTime = Date.now();
|
|
209
|
+
|
|
210
|
+
// Recall last MemCell from Vektor — single fast query, no PPR needed here
|
|
211
|
+
let lastContext = null;
|
|
212
|
+
try {
|
|
213
|
+
const results = await memory.recall(
|
|
214
|
+
`[CLOAK_AXON_MEMCELL] project:${_sessionLog.projectName}`,
|
|
215
|
+
{ limit: 1 }
|
|
216
|
+
);
|
|
217
|
+
if (results && results.length > 0) {
|
|
218
|
+
lastContext = results[0].content;
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// Non-fatal — first session or Vektor not yet seeded
|
|
222
|
+
console.warn('[cloak_axon] Could not load last MemCell:', err.message);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
sessionId : _sessionLog.sessionId,
|
|
227
|
+
projectName : _sessionLog.projectName,
|
|
228
|
+
crashRecovered,
|
|
229
|
+
lastContext,
|
|
230
|
+
message : crashRecovered
|
|
231
|
+
? `Crash recovery: rescued lost session. Last context loaded.`
|
|
232
|
+
: lastContext
|
|
233
|
+
? `Session ${_sessionLog.sessionId} started. Last session context loaded.`
|
|
234
|
+
: `Session ${_sessionLog.sessionId} started. No prior context (first run or new project).`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Hook: onEvent — accumulate session events in RAM
|
|
240
|
+
// Call this from axon or cerebellum when notable things happen.
|
|
241
|
+
// Does NOT write to Vektor — that only happens at Stop.
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* logEvent(type, content)
|
|
246
|
+
* @param {'file_write'|'file_read'|'error'|'fix'|'note'} type
|
|
247
|
+
* @param {string} content
|
|
248
|
+
*/
|
|
249
|
+
function logEvent(type, content) {
|
|
250
|
+
if (!_sessionLog.sessionId) return;
|
|
251
|
+
|
|
252
|
+
const truncated = String(content).slice(0, 200);
|
|
253
|
+
|
|
254
|
+
_sessionLog.events.push({ type, content: truncated, ts: Date.now() });
|
|
255
|
+
|
|
256
|
+
switch (type) {
|
|
257
|
+
case 'file_write':
|
|
258
|
+
_sessionLog.filesWritten.add(path.basename(truncated));
|
|
259
|
+
break;
|
|
260
|
+
case 'file_read':
|
|
261
|
+
_sessionLog.filesRead.add(path.basename(truncated));
|
|
262
|
+
break;
|
|
263
|
+
case 'error':
|
|
264
|
+
_sessionLog.errorsEncountered.push(truncated);
|
|
265
|
+
break;
|
|
266
|
+
case 'fix':
|
|
267
|
+
_sessionLog.fixesApplied.push(truncated);
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Debounced crash recovery write — survives SIGKILL
|
|
272
|
+
_debouncedCrashWrite();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Hook: SessionStop
|
|
277
|
+
// Builds MemCell from RAM log, writes ONE node to Vektor, clears log.
|
|
278
|
+
// This is the ONLY write call in axon — one per session, not continuous.
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* onSessionStop({ memory, tokenCount })
|
|
283
|
+
* Call this when Claude Code fires the Stop hook.
|
|
284
|
+
* @param {object} memory - Vektor memory instance
|
|
285
|
+
* @param {number} tokenCount - Total tokens used this session (from cloak_token)
|
|
286
|
+
* @returns {object} - { sessionId, memCell, written }
|
|
287
|
+
*/
|
|
288
|
+
async function onSessionStop({ memory, tokenCount = 0, narrative = '' } = {}) {
|
|
289
|
+
if (!memory) throw new Error('cloak_axon: memory instance is required');
|
|
290
|
+
|
|
291
|
+
if (!_sessionLog.sessionId) {
|
|
292
|
+
console.warn('[cloak_axon] onSessionStop called without active session');
|
|
293
|
+
return { sessionId: null, memCell: null, written: false };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Cancel any pending debounced crash write — clean stop, no crash
|
|
297
|
+
if (_crashWriteTimer) {
|
|
298
|
+
clearTimeout(_crashWriteTimer);
|
|
299
|
+
_crashWriteTimer = null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Delete crash recovery file — session ended cleanly
|
|
303
|
+
try {
|
|
304
|
+
const crashLogPath = _getCrashLogPath(_sessionLog.projectPath);
|
|
305
|
+
if (fs.existsSync(crashLogPath)) fs.unlinkSync(crashLogPath);
|
|
306
|
+
} catch {}
|
|
307
|
+
|
|
308
|
+
// Build the MemCell — includes narrative intent from final Claude message
|
|
309
|
+
const memCell = buildMemCell(_sessionLog, tokenCount, narrative);
|
|
310
|
+
|
|
311
|
+
let written = false;
|
|
312
|
+
try {
|
|
313
|
+
await memory.remember(memCell);
|
|
314
|
+
written = true;
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error('[cloak_axon] Failed to write MemCell:', err.message);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const result = {
|
|
320
|
+
sessionId : _sessionLog.sessionId,
|
|
321
|
+
memCell,
|
|
322
|
+
written,
|
|
323
|
+
events : _sessionLog.events.length,
|
|
324
|
+
duration : Math.round((Date.now() - _sessionLog.startTime) / 60000),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
_resetLog(); // clear RAM — do not persist raw events
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Claude Code hook registration helper
|
|
333
|
+
// Reads Claude Code's hook payload from stdin (as Claude Code sends it)
|
|
334
|
+
// and routes to the correct handler.
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* handleHookPayload({ payload, memory, projectPath, tokenCount })
|
|
339
|
+
* Use this if integrating directly with Claude Code's hook system.
|
|
340
|
+
* Claude Code sends JSON via stdin to hook scripts.
|
|
341
|
+
*/
|
|
342
|
+
async function handleHookPayload({ payload, memory, projectPath, tokenCount = 0 } = {}) {
|
|
343
|
+
const hookName = payload?.hook_event_name || payload?.event;
|
|
344
|
+
|
|
345
|
+
switch (hookName) {
|
|
346
|
+
case 'SessionStart':
|
|
347
|
+
case 'session_start':
|
|
348
|
+
return onSessionStart({ projectPath: projectPath || payload?.cwd, memory });
|
|
349
|
+
|
|
350
|
+
case 'Stop':
|
|
351
|
+
case 'session_stop': {
|
|
352
|
+
// Extract Claude's final summary message as session narrative
|
|
353
|
+
// Claude Code emits a final assistant message before Stop — this is the "Why"
|
|
354
|
+
const narrative = payload?.last_assistant_message
|
|
355
|
+
|| payload?.session_summary
|
|
356
|
+
|| payload?.final_message
|
|
357
|
+
|| '';
|
|
358
|
+
return onSessionStop({
|
|
359
|
+
memory,
|
|
360
|
+
tokenCount,
|
|
361
|
+
narrative,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
case 'PreToolUse':
|
|
366
|
+
case 'PostToolUse': {
|
|
367
|
+
// Log file reads/writes for MemCell accumulation
|
|
368
|
+
const tool = payload?.tool_name;
|
|
369
|
+
const input = payload?.tool_input;
|
|
370
|
+
if (tool === 'Write' || tool === 'Edit' || tool === 'MultiEdit') {
|
|
371
|
+
logEvent('file_write', input?.file_path || input?.path || 'unknown');
|
|
372
|
+
} else if (tool === 'Read') {
|
|
373
|
+
logEvent('file_read', input?.file_path || input?.path || 'unknown');
|
|
374
|
+
}
|
|
375
|
+
return null; // no write to Vektor on per-tool hooks
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
default:
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = {
|
|
384
|
+
onSessionStart,
|
|
385
|
+
onSessionStop,
|
|
386
|
+
logEvent,
|
|
387
|
+
handleHookPayload,
|
|
388
|
+
_sessionLog, // exported for testing and cloak_token integration
|
|
389
|
+
};
|