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.
Files changed (6) hide show
  1. package/axon.js +389 -0
  2. package/cerebellum.js +439 -0
  3. package/cortex.js +221 -0
  4. package/index.js +480 -0
  5. package/package.json +114 -94
  6. 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
+ };