kernelbot 1.0.37 → 1.0.39

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 (41) hide show
  1. package/bin/kernel.js +499 -249
  2. package/config.example.yaml +17 -0
  3. package/knowledge_base/active_inference_foraging.md +126 -0
  4. package/knowledge_base/index.md +1 -1
  5. package/package.json +3 -1
  6. package/src/agent.js +355 -82
  7. package/src/bot.js +724 -12
  8. package/src/character.js +406 -0
  9. package/src/characters/builder.js +174 -0
  10. package/src/characters/builtins.js +421 -0
  11. package/src/conversation.js +17 -2
  12. package/src/dashboard/agents.css +469 -0
  13. package/src/dashboard/agents.html +184 -0
  14. package/src/dashboard/agents.js +873 -0
  15. package/src/dashboard/dashboard.css +281 -0
  16. package/src/dashboard/dashboard.js +579 -0
  17. package/src/dashboard/index.html +366 -0
  18. package/src/dashboard/server.js +521 -0
  19. package/src/dashboard/shared.css +700 -0
  20. package/src/dashboard/shared.js +218 -0
  21. package/src/life/engine.js +115 -26
  22. package/src/life/evolution.js +7 -5
  23. package/src/life/journal.js +5 -4
  24. package/src/life/memory.js +12 -9
  25. package/src/life/share-queue.js +7 -5
  26. package/src/prompts/orchestrator.js +76 -14
  27. package/src/prompts/workers.js +22 -0
  28. package/src/self.js +17 -5
  29. package/src/services/linkedin-api.js +190 -0
  30. package/src/services/stt.js +8 -2
  31. package/src/services/tts.js +32 -2
  32. package/src/services/x-api.js +141 -0
  33. package/src/swarm/worker-registry.js +7 -0
  34. package/src/tools/categories.js +4 -0
  35. package/src/tools/index.js +6 -0
  36. package/src/tools/linkedin.js +264 -0
  37. package/src/tools/orchestrator-tools.js +337 -2
  38. package/src/tools/x.js +256 -0
  39. package/src/utils/config.js +190 -139
  40. package/src/utils/display.js +165 -52
  41. package/src/utils/temporal-awareness.js +24 -10
@@ -0,0 +1,218 @@
1
+ /**
2
+ * KERNEL Dashboard — Shared utilities, SSE, gauges, canvas animations.
3
+ * Exposed on window.KERNEL for use by page-specific scripts.
4
+ */
5
+ (function() {
6
+ // ── Utilities ──
7
+ function esc(s) {
8
+ if (!s) return '';
9
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
10
+ }
11
+
12
+ function formatDuration(seconds) {
13
+ if (seconds == null || seconds < 0) return '--';
14
+ const s = Math.floor(seconds);
15
+ if (s < 60) return s + 's';
16
+ if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's';
17
+ return Math.floor(s/3600) + 'h ' + Math.floor((s%3600)/60) + 'm';
18
+ }
19
+
20
+ function timeAgo(ts) {
21
+ if (!ts) return 'never';
22
+ const s = Math.floor((Date.now() - ts)/1000);
23
+ if (s < 60) return 'just now';
24
+ if (s < 3600) return Math.floor(s/60) + 'm ago';
25
+ if (s < 86400) return Math.floor(s/3600) + 'h ago';
26
+ return Math.floor(s/86400) + 'd ago';
27
+ }
28
+
29
+ function formatBytes(b) {
30
+ if (b < 1024) return b + ' B';
31
+ if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
32
+ if (b < 1073741824) return (b/1048576).toFixed(1) + ' MB';
33
+ return (b/1073741824).toFixed(2) + ' GB';
34
+ }
35
+
36
+ function barColor(pct) { return pct < 50 ? 'green' : pct < 80 ? 'amber' : 'red'; }
37
+
38
+ function makeBar(label, pct, color) {
39
+ return `<div class="row"><span class="k">${esc(label)}</span><span class="v">${pct.toFixed(1)}%</span></div><div class="bar-track"><div class="bar-fill ${color || barColor(pct)}" style="width:${Math.min(pct,100)}%"></div></div>`;
40
+ }
41
+
42
+ const $ = id => document.getElementById(id);
43
+
44
+ // ── Clock ──
45
+ function startClock() {
46
+ setInterval(() => {
47
+ const now = new Date();
48
+ const p = n => String(n).padStart(2,'0');
49
+ const full = now.getFullYear() + '.' + p(now.getMonth()+1) + '.' + p(now.getDate()) + ' ' + p(now.getHours()) + ':' + p(now.getMinutes()) + ':' + p(now.getSeconds());
50
+ const hdrClock = $('hdr-clock');
51
+ if (hdrClock) hdrClock.textContent = full;
52
+ const rbClock = $('rb-clock');
53
+ if (rbClock) rbClock.textContent = p(now.getHours()) + ':' + p(now.getMinutes());
54
+ }, 1000);
55
+ }
56
+
57
+ // ── Gauge helpers ──
58
+ function setGauge(id, pct, label) {
59
+ const el = $(id);
60
+ if (!el) return;
61
+ const circumference = parseFloat(el.querySelector('.fill').getAttribute('stroke-dasharray'));
62
+ const offset = circumference * (1 - Math.min(pct, 100) / 100);
63
+ const fill = el.querySelector('.fill');
64
+ fill.style.strokeDashoffset = offset;
65
+ fill.classList.remove('amber', 'red');
66
+ if (pct >= 80) fill.classList.add('red');
67
+ else if (pct >= 50) fill.classList.add('amber');
68
+ const pctEl = el.querySelector('.pct');
69
+ if (pctEl) {
70
+ pctEl.textContent = pct.toFixed(0) + '%';
71
+ pctEl.classList.remove('amber','red');
72
+ if (pct >= 80) pctEl.classList.add('red');
73
+ else if (pct >= 50) pctEl.classList.add('amber');
74
+ }
75
+ const sub = el.querySelector('.sub');
76
+ if (sub && label) sub.textContent = label;
77
+ }
78
+
79
+ function setMiniGauge(id, pct) {
80
+ const el = $(id);
81
+ if (!el) return;
82
+ const fill = el.querySelector('.fill');
83
+ const circumference = parseFloat(fill.getAttribute('stroke-dasharray'));
84
+ fill.style.strokeDashoffset = circumference * (1 - Math.min(pct, 100) / 100);
85
+ fill.classList.remove('amber','red');
86
+ if (pct >= 80) fill.classList.add('red');
87
+ else if (pct >= 50) fill.classList.add('amber');
88
+ el.querySelector('.val').textContent = pct.toFixed(0) + '%';
89
+ }
90
+
91
+ // ── SSE Connection ──
92
+ function connectSSE(onSnapshot) {
93
+ let reconnectDelay = 1000;
94
+ function connect() {
95
+ const es = new EventSource('/events');
96
+ es.onopen = () => {
97
+ reconnectDelay = 1000;
98
+ const connDot = $('conn-dot');
99
+ if (connDot) connDot.classList.add('connected');
100
+ const topConn = $('top-conn');
101
+ if (topConn) topConn.classList.add('connected');
102
+ const hdrStatus = $('hdr-status');
103
+ if (hdrStatus) hdrStatus.textContent = 'ONLINE';
104
+ };
105
+ es.onmessage = (evt) => {
106
+ try {
107
+ const data = JSON.parse(evt.data);
108
+ onSnapshot(data);
109
+ } catch(e) {
110
+ if (e instanceof SyntaxError) {
111
+ console.debug('[Dashboard] Malformed SSE message (invalid JSON)');
112
+ } else {
113
+ console.warn('[Dashboard] Snapshot handler error:', e.message || e);
114
+ }
115
+ }
116
+ };
117
+ es.onerror = () => {
118
+ es.close();
119
+ const connDot = $('conn-dot');
120
+ if (connDot) connDot.classList.remove('connected');
121
+ const topConn = $('top-conn');
122
+ if (topConn) topConn.classList.remove('connected');
123
+ const hdrStatus = $('hdr-status');
124
+ if (hdrStatus) hdrStatus.textContent = 'RECONNECTING';
125
+ setTimeout(connect, reconnectDelay);
126
+ reconnectDelay = Math.min(reconnectDelay * 1.5, 15000);
127
+ };
128
+ }
129
+ connect();
130
+ }
131
+
132
+ // ── Particle Grid Canvas ──
133
+ function initParticleCanvas() {
134
+ const canvas = $('particle-canvas');
135
+ if (!canvas) return;
136
+ const ctx = canvas.getContext('2d');
137
+ let w, h;
138
+ const GRID = 50;
139
+
140
+ function resize() {
141
+ w = canvas.width = window.innerWidth;
142
+ h = canvas.height = window.innerHeight;
143
+ drawGrid();
144
+ }
145
+
146
+ function drawGrid() {
147
+ ctx.clearRect(0, 0, w, h);
148
+ ctx.strokeStyle = 'rgba(57,255,20,0.02)';
149
+ ctx.lineWidth = 0.5;
150
+ for (let x = 0; x <= w; x += GRID) {
151
+ ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke();
152
+ }
153
+ for (let y = 0; y <= h; y += GRID) {
154
+ ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
155
+ }
156
+ }
157
+
158
+ resize();
159
+ window.addEventListener('resize', resize);
160
+ }
161
+
162
+ // ── Waveform Canvas ──
163
+ function initWaveform() {
164
+ const canvas = $('waveform-canvas');
165
+ if (!canvas) return;
166
+ const ctx = canvas.getContext('2d');
167
+ let w, h, phase = 0;
168
+
169
+ function resize() {
170
+ const parent = canvas.parentElement;
171
+ w = canvas.width = parent.offsetWidth;
172
+ h = canvas.height = parent.offsetHeight;
173
+ }
174
+ resize();
175
+ window.addEventListener('resize', resize);
176
+
177
+ const waves = [
178
+ { freq: 0.015, amp: 0.25, speed: 0.02, alpha: 0.5, width: 1.5 },
179
+ { freq: 0.025, amp: 0.15, speed: 0.035, alpha: 0.3, width: 1 },
180
+ { freq: 0.04, amp: 0.08, speed: 0.05, alpha: 0.15, width: 0.8 },
181
+ ];
182
+
183
+ function draw() {
184
+ requestAnimationFrame(draw);
185
+ ctx.clearRect(0, 0, w, h);
186
+ for (const wave of waves) {
187
+ ctx.beginPath();
188
+ ctx.strokeStyle = `rgba(57,255,20,${wave.alpha})`;
189
+ ctx.lineWidth = wave.width;
190
+ for (let x = 0; x < w; x++) {
191
+ const y = h/2 + Math.sin(x * wave.freq + phase * wave.speed * 60) * h * wave.amp
192
+ + Math.sin(x * wave.freq * 2.3 + phase * wave.speed * 40) * h * wave.amp * 0.4;
193
+ x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
194
+ }
195
+ ctx.stroke();
196
+ }
197
+ phase += 0.016;
198
+ }
199
+ requestAnimationFrame(draw);
200
+ }
201
+
202
+ // ── Expose on window.KERNEL ──
203
+ window.KERNEL = {
204
+ esc,
205
+ formatDuration,
206
+ timeAgo,
207
+ formatBytes,
208
+ barColor,
209
+ makeBar,
210
+ $,
211
+ startClock,
212
+ setGauge,
213
+ setMiniGauge,
214
+ connectSSE,
215
+ initParticleCanvas,
216
+ initWaveform,
217
+ };
218
+ })();
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, openSync, readSync, closeSync, statSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
4
  import { getLogger } from '../utils/logger.js';
@@ -8,7 +8,7 @@ const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
8
8
  const STATE_FILE = join(LIFE_DIR, 'state.json');
9
9
  const IDEAS_FILE = join(LIFE_DIR, 'ideas.json');
10
10
 
11
- const LIFE_CHAT_ID = '__life__';
11
+ const DEFAULT_LIFE_CHAT_ID = '__life__';
12
12
  const LIFE_USER = { id: 'life_engine', username: 'inner_self' };
13
13
 
14
14
  const DEFAULT_STATE = {
@@ -22,6 +22,8 @@ const DEFAULT_STATE = {
22
22
  activityCounts: { think: 0, browse: 0, journal: 0, create: 0, self_code: 0, code_review: 0, reflect: 0 },
23
23
  paused: false,
24
24
  lastWakeUp: null,
25
+ // Failure tracking: consecutive failures per activity type
26
+ activityFailures: {},
25
27
  };
26
28
 
27
29
  const LOG_FILE_PATHS = [
@@ -33,7 +35,7 @@ export class LifeEngine {
33
35
  /**
34
36
  * @param {{ config: object, agent: object, memoryManager: object, journalManager: object, shareQueue: object, improvementTracker?: object, evolutionTracker?: object, codebaseKnowledge?: object, selfManager: object }} deps
35
37
  */
36
- constructor({ config, agent, memoryManager, journalManager, shareQueue, improvementTracker, evolutionTracker, codebaseKnowledge, selfManager }) {
38
+ constructor({ config, agent, memoryManager, journalManager, shareQueue, improvementTracker, evolutionTracker, codebaseKnowledge, selfManager, basePath = null, characterId = null }) {
37
39
  this.config = config;
38
40
  this.agent = agent;
39
41
  this.memoryManager = memoryManager;
@@ -46,17 +48,24 @@ export class LifeEngine {
46
48
  this.selfManager = selfManager;
47
49
  this._timerId = null;
48
50
  this._status = 'idle'; // idle, active, paused
51
+ this._characterId = characterId || null;
49
52
 
50
- mkdirSync(LIFE_DIR, { recursive: true });
53
+ this._lifeDir = basePath || LIFE_DIR;
54
+ this._stateFile = join(this._lifeDir, 'state.json');
55
+ this._ideasFile = join(this._lifeDir, 'ideas.json');
56
+
57
+ this._lifeChatId = this._characterId ? `__life__:${this._characterId}` : DEFAULT_LIFE_CHAT_ID;
58
+
59
+ mkdirSync(this._lifeDir, { recursive: true });
51
60
  this._state = this._loadState();
52
61
  }
53
62
 
54
63
  // ── State Persistence ──────────────────────────────────────────
55
64
 
56
65
  _loadState() {
57
- if (existsSync(STATE_FILE)) {
66
+ if (existsSync(this._stateFile)) {
58
67
  try {
59
- return { ...DEFAULT_STATE, ...JSON.parse(readFileSync(STATE_FILE, 'utf-8')) };
68
+ return { ...DEFAULT_STATE, ...JSON.parse(readFileSync(this._stateFile, 'utf-8')) };
60
69
  } catch {
61
70
  return { ...DEFAULT_STATE };
62
71
  }
@@ -65,20 +74,20 @@ export class LifeEngine {
65
74
  }
66
75
 
67
76
  _saveState() {
68
- writeFileSync(STATE_FILE, JSON.stringify(this._state, null, 2), 'utf-8');
77
+ writeFileSync(this._stateFile, JSON.stringify(this._state, null, 2), 'utf-8');
69
78
  }
70
79
 
71
80
  // ── Ideas Backlog ──────────────────────────────────────────────
72
81
 
73
82
  _loadIdeas() {
74
- if (existsSync(IDEAS_FILE)) {
75
- try { return JSON.parse(readFileSync(IDEAS_FILE, 'utf-8')); } catch { return []; }
83
+ if (existsSync(this._ideasFile)) {
84
+ try { return JSON.parse(readFileSync(this._ideasFile, 'utf-8')); } catch { return []; }
76
85
  }
77
86
  return [];
78
87
  }
79
88
 
80
89
  _saveIdeas(ideas) {
81
- writeFileSync(IDEAS_FILE, JSON.stringify(ideas, null, 2), 'utf-8');
90
+ writeFileSync(this._ideasFile, JSON.stringify(ideas, null, 2), 'utf-8');
82
91
  }
83
92
 
84
93
  _addIdea(idea) {
@@ -166,6 +175,12 @@ export class LifeEngine {
166
175
  ? Math.round((Date.now() - this._state.lastWakeUp) / 60000)
167
176
  : null;
168
177
 
178
+ // Summarise suppressed activities (3+ consecutive failures)
179
+ const failures = this._state.activityFailures || {};
180
+ const suppressedActivities = Object.entries(failures)
181
+ .filter(([, info]) => info.count >= 3)
182
+ .map(([type, info]) => type);
183
+
169
184
  return {
170
185
  status: this._status,
171
186
  paused: this._state.paused,
@@ -175,6 +190,7 @@ export class LifeEngine {
175
190
  lastActivity: this._state.lastActivity,
176
191
  lastActivityAgo: lastAgo !== null ? `${lastAgo}m` : 'never',
177
192
  lastWakeUpAgo: wakeAgo !== null ? `${wakeAgo}m` : 'never',
193
+ suppressedActivities,
178
194
  };
179
195
  }
180
196
 
@@ -206,10 +222,32 @@ export class LifeEngine {
206
222
  const activityType = this._selectActivity();
207
223
  logger.info(`[LifeEngine] Heartbeat tick — selected: ${activityType}`);
208
224
 
225
+ const startTime = Date.now();
209
226
  try {
210
227
  await this._executeActivity(activityType);
228
+ const durationSec = ((Date.now() - startTime) / 1000).toFixed(1);
229
+ logger.info(`[LifeEngine] Activity "${activityType}" completed in ${durationSec}s`);
230
+ // Clear failure streak on success
231
+ if (this._state.activityFailures?.[activityType]) {
232
+ delete this._state.activityFailures[activityType];
233
+ this._saveState();
234
+ }
211
235
  } catch (err) {
212
- logger.error(`[LifeEngine] Activity "${activityType}" failed: ${err.message}`);
236
+ const durationSec = ((Date.now() - startTime) / 1000).toFixed(1);
237
+ // Track consecutive failures per activity type
238
+ if (!this._state.activityFailures) this._state.activityFailures = {};
239
+ const prev = this._state.activityFailures[activityType] || { count: 0 };
240
+ this._state.activityFailures[activityType] = {
241
+ count: prev.count + 1,
242
+ lastFailure: Date.now(),
243
+ lastError: err.message?.slice(0, 200),
244
+ };
245
+ this._saveState();
246
+ const failCount = this._state.activityFailures[activityType].count;
247
+ logger.error(`[LifeEngine] Activity "${activityType}" failed after ${durationSec}s (streak: ${failCount}): ${err.message}`);
248
+ if (failCount >= 3) {
249
+ logger.warn(`[LifeEngine] Activity "${activityType}" suppressed after ${failCount} consecutive failures — will auto-recover in 1h`);
250
+ }
213
251
  }
214
252
 
215
253
  // Re-arm for next tick
@@ -221,6 +259,7 @@ export class LifeEngine {
221
259
  // ── Activity Selection ─────────────────────────────────────────
222
260
 
223
261
  _selectActivity() {
262
+ const logger = getLogger();
224
263
  const lifeConfig = this.config.life || {};
225
264
  const selfCodingConfig = lifeConfig.self_coding || {};
226
265
  const weights = {
@@ -238,29 +277,44 @@ export class LifeEngine {
238
277
  // Rule: don't repeat same type twice in a row
239
278
  const last = this._state.lastActivity;
240
279
 
241
- // Rule: journal cooldown 4h
242
- if (this._state.lastJournalTime && now - this._state.lastJournalTime < 4 * 3600_000) {
280
+ // Cooldown durations (hours) — all configurable via life config, with sensible defaults
281
+ const journalCooldownMs = (lifeConfig.cooldown_hours?.journal ?? 4) * 3600_000;
282
+ const reflectCooldownMs = (lifeConfig.cooldown_hours?.reflect ?? 4) * 3600_000;
283
+ const selfCodingEnabled = selfCodingConfig.enabled === true;
284
+ const selfCodeCooldownMs = (selfCodingConfig.cooldown_hours ?? 2) * 3600_000;
285
+ const codeReviewCooldownMs = (selfCodingConfig.code_review_cooldown_hours ?? 4) * 3600_000;
286
+
287
+ // Apply cooldown rules
288
+ if (this._state.lastJournalTime && now - this._state.lastJournalTime < journalCooldownMs) {
243
289
  weights.journal = 0;
244
290
  }
245
291
 
246
- // Rule: self_code cooldown (configurable, default 2h) + must be enabled
247
- const selfCodingEnabled = selfCodingConfig.enabled === true;
248
- const selfCodeCooldownMs = (selfCodingConfig.cooldown_hours ?? 2) * 3600_000;
249
292
  if (!selfCodingEnabled || (this._state.lastSelfCodeTime && now - this._state.lastSelfCodeTime < selfCodeCooldownMs)) {
250
293
  weights.self_code = 0;
251
294
  }
252
295
 
253
- // Rule: code_review cooldown (configurable, default 4h) + must have evolution tracker
254
- const codeReviewCooldownMs = (selfCodingConfig.code_review_cooldown_hours ?? 4) * 3600_000;
255
296
  if (!selfCodingEnabled || !this.evolutionTracker || (this._state.lastCodeReviewTime && now - this._state.lastCodeReviewTime < codeReviewCooldownMs)) {
256
297
  weights.code_review = 0;
257
298
  }
258
299
 
259
- // Rule: reflect cooldown 4h
260
- if (this._state.lastReflectTime && now - this._state.lastReflectTime < 4 * 3600_000) {
300
+ if (this._state.lastReflectTime && now - this._state.lastReflectTime < reflectCooldownMs) {
261
301
  weights.reflect = 0;
262
302
  }
263
303
 
304
+ // Suppress activity types that have failed repeatedly (3+ consecutive failures)
305
+ const failures = this._state.activityFailures || {};
306
+ for (const [type, info] of Object.entries(failures)) {
307
+ if (weights[type] !== undefined && info.count >= 3) {
308
+ // Auto-recover after 1 hour since last failure
309
+ if (info.lastFailure && now - info.lastFailure > 3600_000) {
310
+ delete failures[type];
311
+ } else {
312
+ weights[type] = 0;
313
+ logger.debug(`[LifeEngine] Suppressing "${type}" due to ${info.count} consecutive failures`);
314
+ }
315
+ }
316
+ }
317
+
264
318
  // Remove last activity from options (no repeats)
265
319
  if (last && weights[last] !== undefined) {
266
320
  weights[last] = 0;
@@ -1180,16 +1234,51 @@ Be honest and constructive. This is your chance to learn from real interactions.
1180
1234
  }
1181
1235
 
1182
1236
  /**
1183
- * Read recent log entries from kernel.log.
1184
- * Returns parsed JSON entries or null if no logs available.
1237
+ * Read recent log entries from kernel.log using an efficient tail strategy.
1238
+ *
1239
+ * Instead of loading the entire log file into memory (which can be many MB
1240
+ * for a long-running bot), this reads only the last chunk of the file
1241
+ * (default 64 KB) and extracts lines from that. This keeps memory usage
1242
+ * bounded regardless of total log size.
1243
+ *
1244
+ * @param {number} maxLines - Maximum number of recent log lines to return.
1245
+ * @returns {Array<object>|null} Parsed JSON log entries, or null if unavailable.
1185
1246
  */
1186
1247
  _readRecentLogs(maxLines = 200) {
1248
+ // 64 KB is enough to hold ~200+ JSON log lines (avg ~300 bytes each)
1249
+ const TAIL_BYTES = 64 * 1024;
1250
+
1187
1251
  for (const logPath of LOG_FILE_PATHS) {
1188
1252
  if (!existsSync(logPath)) continue;
1189
1253
 
1190
1254
  try {
1191
- const content = readFileSync(logPath, 'utf-8');
1192
- const lines = content.split('\n').filter(Boolean);
1255
+ const fileSize = statSync(logPath).size;
1256
+ if (fileSize === 0) continue;
1257
+
1258
+ let tailContent;
1259
+
1260
+ if (fileSize <= TAIL_BYTES) {
1261
+ // File is small enough to read entirely
1262
+ tailContent = readFileSync(logPath, 'utf-8');
1263
+ } else {
1264
+ // Read only the last TAIL_BYTES from the file
1265
+ const fd = openSync(logPath, 'r');
1266
+ try {
1267
+ const buffer = Buffer.alloc(TAIL_BYTES);
1268
+ const startPos = fileSize - TAIL_BYTES;
1269
+ readSync(fd, buffer, 0, TAIL_BYTES, startPos);
1270
+ tailContent = buffer.toString('utf-8');
1271
+ // Drop the first (likely partial) line since we started mid-file
1272
+ const firstNewline = tailContent.indexOf('\n');
1273
+ if (firstNewline !== -1) {
1274
+ tailContent = tailContent.slice(firstNewline + 1);
1275
+ }
1276
+ } finally {
1277
+ closeSync(fd);
1278
+ }
1279
+ }
1280
+
1281
+ const lines = tailContent.split('\n').filter(Boolean);
1193
1282
  const recent = lines.slice(-maxLines);
1194
1283
 
1195
1284
  const entries = [];
@@ -1218,7 +1307,7 @@ Be honest and constructive. This is your chance to learn from real interactions.
1218
1307
  const logger = getLogger();
1219
1308
  try {
1220
1309
  const response = await this.agent.orchestratorProvider.chat({
1221
- system: this.agent._getSystemPrompt(LIFE_CHAT_ID, LIFE_USER),
1310
+ system: this.agent._getSystemPrompt(this._lifeChatId, LIFE_USER),
1222
1311
  messages: [{ role: 'user', content: prompt }],
1223
1312
  });
1224
1313
  return response.text || null;
@@ -1238,7 +1327,7 @@ Be honest and constructive. This is your chance to learn from real interactions.
1238
1327
  // Use the agent's processMessage to go through the full orchestrator pipeline
1239
1328
  // The orchestrator will see the task and dispatch appropriately
1240
1329
  const response = await this.agent.processMessage(
1241
- LIFE_CHAT_ID,
1330
+ this._lifeChatId,
1242
1331
  task,
1243
1332
  LIFE_USER,
1244
1333
  // No-op onUpdate — life engine activities are silent
@@ -18,15 +18,17 @@ const DEFAULT_DATA = {
18
18
  };
19
19
 
20
20
  export class EvolutionTracker {
21
- constructor() {
22
- mkdirSync(LIFE_DIR, { recursive: true });
21
+ constructor(basePath = null) {
22
+ const lifeDir = basePath || LIFE_DIR;
23
+ this._evolutionFile = join(lifeDir, 'evolution.json');
24
+ mkdirSync(lifeDir, { recursive: true });
23
25
  this._data = this._load();
24
26
  }
25
27
 
26
28
  _load() {
27
- if (existsSync(EVOLUTION_FILE)) {
29
+ if (existsSync(this._evolutionFile)) {
28
30
  try {
29
- const raw = JSON.parse(readFileSync(EVOLUTION_FILE, 'utf-8'));
31
+ const raw = JSON.parse(readFileSync(this._evolutionFile, 'utf-8'));
30
32
  return {
31
33
  proposals: raw.proposals || [],
32
34
  lessons: raw.lessons || [],
@@ -40,7 +42,7 @@ export class EvolutionTracker {
40
42
  }
41
43
 
42
44
  _save() {
43
- writeFileSync(EVOLUTION_FILE, JSON.stringify(this._data, null, 2), 'utf-8');
45
+ writeFileSync(this._evolutionFile, JSON.stringify(this._data, null, 2), 'utf-8');
44
46
  }
45
47
 
46
48
  _recalcStats() {
@@ -20,12 +20,13 @@ function timeNow() {
20
20
  }
21
21
 
22
22
  export class JournalManager {
23
- constructor() {
24
- mkdirSync(JOURNAL_DIR, { recursive: true });
23
+ constructor(basePath = null) {
24
+ this._dir = basePath || JOURNAL_DIR;
25
+ mkdirSync(this._dir, { recursive: true });
25
26
  }
26
27
 
27
28
  _journalPath(date) {
28
- return join(JOURNAL_DIR, `${date}.md`);
29
+ return join(this._dir, `${date}.md`);
29
30
  }
30
31
 
31
32
  /**
@@ -93,7 +94,7 @@ export class JournalManager {
93
94
  */
94
95
  list(limit = 30) {
95
96
  try {
96
- const files = readdirSync(JOURNAL_DIR)
97
+ const files = readdirSync(this._dir)
97
98
  .filter(f => f.endsWith('.md'))
98
99
  .map(f => f.replace('.md', ''))
99
100
  .sort()
@@ -10,17 +10,20 @@ const EPISODIC_DIR = join(LIFE_DIR, 'memories', 'episodic');
10
10
  const SEMANTIC_FILE = join(LIFE_DIR, 'memories', 'semantic', 'topics.json');
11
11
 
12
12
  export class MemoryManager {
13
- constructor() {
13
+ constructor(basePath = null) {
14
+ const lifeDir = basePath || LIFE_DIR;
15
+ this._episodicDir = join(lifeDir, 'memories', 'episodic');
16
+ this._semanticFile = join(lifeDir, 'memories', 'semantic', 'topics.json');
14
17
  this._episodicCache = new Map(); // date -> array
15
18
  this._semanticCache = null;
16
- mkdirSync(EPISODIC_DIR, { recursive: true });
17
- mkdirSync(join(LIFE_DIR, 'memories', 'semantic'), { recursive: true });
19
+ mkdirSync(this._episodicDir, { recursive: true });
20
+ mkdirSync(join(lifeDir, 'memories', 'semantic'), { recursive: true });
18
21
  }
19
22
 
20
23
  // ── Episodic Memories ──────────────────────────────────────────
21
24
 
22
25
  _episodicPath(date) {
23
- return join(EPISODIC_DIR, `${date}.json`);
26
+ return join(this._episodicDir, `${date}.json`);
24
27
  }
25
28
 
26
29
  _loadEpisodicDay(date) {
@@ -136,11 +139,11 @@ export class MemoryManager {
136
139
  let pruned = 0;
137
140
 
138
141
  try {
139
- const files = readdirSync(EPISODIC_DIR).filter(f => f.endsWith('.json'));
142
+ const files = readdirSync(this._episodicDir).filter(f => f.endsWith('.json'));
140
143
  for (const file of files) {
141
144
  const date = file.replace('.json', '');
142
145
  if (date < cutoffDate) {
143
- unlinkSync(join(EPISODIC_DIR, file));
146
+ unlinkSync(join(this._episodicDir, file));
144
147
  this._episodicCache.delete(date);
145
148
  pruned++;
146
149
  }
@@ -168,9 +171,9 @@ export class MemoryManager {
168
171
 
169
172
  _loadSemantic() {
170
173
  if (this._semanticCache) return this._semanticCache;
171
- if (existsSync(SEMANTIC_FILE)) {
174
+ if (existsSync(this._semanticFile)) {
172
175
  try {
173
- this._semanticCache = JSON.parse(readFileSync(SEMANTIC_FILE, 'utf-8'));
176
+ this._semanticCache = JSON.parse(readFileSync(this._semanticFile, 'utf-8'));
174
177
  } catch {
175
178
  this._semanticCache = {};
176
179
  }
@@ -181,7 +184,7 @@ export class MemoryManager {
181
184
  }
182
185
 
183
186
  _saveSemantic() {
184
- writeFileSync(SEMANTIC_FILE, JSON.stringify(this._semanticCache || {}, null, 2), 'utf-8');
187
+ writeFileSync(this._semanticFile, JSON.stringify(this._semanticCache || {}, null, 2), 'utf-8');
185
188
  }
186
189
 
187
190
  /**
@@ -9,15 +9,17 @@ const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
9
9
  const SHARES_FILE = join(LIFE_DIR, 'shares.json');
10
10
 
11
11
  export class ShareQueue {
12
- constructor() {
13
- mkdirSync(LIFE_DIR, { recursive: true });
12
+ constructor(basePath = null) {
13
+ const lifeDir = basePath || LIFE_DIR;
14
+ this._sharesFile = join(lifeDir, 'shares.json');
15
+ mkdirSync(lifeDir, { recursive: true });
14
16
  this._data = this._load();
15
17
  }
16
18
 
17
19
  _load() {
18
- if (existsSync(SHARES_FILE)) {
20
+ if (existsSync(this._sharesFile)) {
19
21
  try {
20
- return JSON.parse(readFileSync(SHARES_FILE, 'utf-8'));
22
+ return JSON.parse(readFileSync(this._sharesFile, 'utf-8'));
21
23
  } catch {
22
24
  return { pending: [], shared: [] };
23
25
  }
@@ -26,7 +28,7 @@ export class ShareQueue {
26
28
  }
27
29
 
28
30
  _save() {
29
- writeFileSync(SHARES_FILE, JSON.stringify(this._data, null, 2), 'utf-8');
31
+ writeFileSync(this._sharesFile, JSON.stringify(this._data, null, 2), 'utf-8');
30
32
  }
31
33
 
32
34
  /**