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.
- package/bin/kernel.js +499 -249
- package/config.example.yaml +17 -0
- package/knowledge_base/active_inference_foraging.md +126 -0
- package/knowledge_base/index.md +1 -1
- package/package.json +3 -1
- package/src/agent.js +355 -82
- package/src/bot.js +724 -12
- package/src/character.js +406 -0
- package/src/characters/builder.js +174 -0
- package/src/characters/builtins.js +421 -0
- package/src/conversation.js +17 -2
- package/src/dashboard/agents.css +469 -0
- package/src/dashboard/agents.html +184 -0
- package/src/dashboard/agents.js +873 -0
- package/src/dashboard/dashboard.css +281 -0
- package/src/dashboard/dashboard.js +579 -0
- package/src/dashboard/index.html +366 -0
- package/src/dashboard/server.js +521 -0
- package/src/dashboard/shared.css +700 -0
- package/src/dashboard/shared.js +218 -0
- package/src/life/engine.js +115 -26
- package/src/life/evolution.js +7 -5
- package/src/life/journal.js +5 -4
- package/src/life/memory.js +12 -9
- package/src/life/share-queue.js +7 -5
- package/src/prompts/orchestrator.js +76 -14
- package/src/prompts/workers.js +22 -0
- package/src/self.js +17 -5
- package/src/services/linkedin-api.js +190 -0
- package/src/services/stt.js +8 -2
- package/src/services/tts.js +32 -2
- package/src/services/x-api.js +141 -0
- package/src/swarm/worker-registry.js +7 -0
- package/src/tools/categories.js +4 -0
- package/src/tools/index.js +6 -0
- package/src/tools/linkedin.js +264 -0
- package/src/tools/orchestrator-tools.js +337 -2
- package/src/tools/x.js +256 -0
- package/src/utils/config.js +190 -139
- package/src/utils/display.js +165 -52
- 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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
|
+
})();
|
package/src/life/engine.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
66
|
+
if (existsSync(this._stateFile)) {
|
|
58
67
|
try {
|
|
59
|
-
return { ...DEFAULT_STATE, ...JSON.parse(readFileSync(
|
|
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(
|
|
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(
|
|
75
|
-
try { return JSON.parse(readFileSync(
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
1192
|
-
|
|
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(
|
|
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
|
-
|
|
1330
|
+
this._lifeChatId,
|
|
1242
1331
|
task,
|
|
1243
1332
|
LIFE_USER,
|
|
1244
1333
|
// No-op onUpdate — life engine activities are silent
|
package/src/life/evolution.js
CHANGED
|
@@ -18,15 +18,17 @@ const DEFAULT_DATA = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export class EvolutionTracker {
|
|
21
|
-
constructor() {
|
|
22
|
-
|
|
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(
|
|
29
|
+
if (existsSync(this._evolutionFile)) {
|
|
28
30
|
try {
|
|
29
|
-
const raw = JSON.parse(readFileSync(
|
|
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(
|
|
45
|
+
writeFileSync(this._evolutionFile, JSON.stringify(this._data, null, 2), 'utf-8');
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
_recalcStats() {
|
package/src/life/journal.js
CHANGED
|
@@ -20,12 +20,13 @@ function timeNow() {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export class JournalManager {
|
|
23
|
-
constructor() {
|
|
24
|
-
|
|
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(
|
|
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(
|
|
97
|
+
const files = readdirSync(this._dir)
|
|
97
98
|
.filter(f => f.endsWith('.md'))
|
|
98
99
|
.map(f => f.replace('.md', ''))
|
|
99
100
|
.sort()
|
package/src/life/memory.js
CHANGED
|
@@ -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(
|
|
17
|
-
mkdirSync(join(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
174
|
+
if (existsSync(this._semanticFile)) {
|
|
172
175
|
try {
|
|
173
|
-
this._semanticCache = JSON.parse(readFileSync(
|
|
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(
|
|
187
|
+
writeFileSync(this._semanticFile, JSON.stringify(this._semanticCache || {}, null, 2), 'utf-8');
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
/**
|
package/src/life/share-queue.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
20
|
+
if (existsSync(this._sharesFile)) {
|
|
19
21
|
try {
|
|
20
|
-
return JSON.parse(readFileSync(
|
|
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(
|
|
31
|
+
writeFileSync(this._sharesFile, JSON.stringify(this._data, null, 2), 'utf-8');
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|