kernelbot 1.0.37 → 1.0.38

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.
@@ -0,0 +1,209 @@
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 { onSnapshot(JSON.parse(evt.data)); } catch(e) {}
107
+ };
108
+ es.onerror = () => {
109
+ es.close();
110
+ const connDot = $('conn-dot');
111
+ if (connDot) connDot.classList.remove('connected');
112
+ const topConn = $('top-conn');
113
+ if (topConn) topConn.classList.remove('connected');
114
+ const hdrStatus = $('hdr-status');
115
+ if (hdrStatus) hdrStatus.textContent = 'RECONNECTING';
116
+ setTimeout(connect, reconnectDelay);
117
+ reconnectDelay = Math.min(reconnectDelay * 1.5, 15000);
118
+ };
119
+ }
120
+ connect();
121
+ }
122
+
123
+ // ── Particle Grid Canvas ──
124
+ function initParticleCanvas() {
125
+ const canvas = $('particle-canvas');
126
+ if (!canvas) return;
127
+ const ctx = canvas.getContext('2d');
128
+ let w, h;
129
+ const GRID = 50;
130
+
131
+ function resize() {
132
+ w = canvas.width = window.innerWidth;
133
+ h = canvas.height = window.innerHeight;
134
+ drawGrid();
135
+ }
136
+
137
+ function drawGrid() {
138
+ ctx.clearRect(0, 0, w, h);
139
+ ctx.strokeStyle = 'rgba(57,255,20,0.02)';
140
+ ctx.lineWidth = 0.5;
141
+ for (let x = 0; x <= w; x += GRID) {
142
+ ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke();
143
+ }
144
+ for (let y = 0; y <= h; y += GRID) {
145
+ ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
146
+ }
147
+ }
148
+
149
+ resize();
150
+ window.addEventListener('resize', resize);
151
+ }
152
+
153
+ // ── Waveform Canvas ──
154
+ function initWaveform() {
155
+ const canvas = $('waveform-canvas');
156
+ if (!canvas) return;
157
+ const ctx = canvas.getContext('2d');
158
+ let w, h, phase = 0;
159
+
160
+ function resize() {
161
+ const parent = canvas.parentElement;
162
+ w = canvas.width = parent.offsetWidth;
163
+ h = canvas.height = parent.offsetHeight;
164
+ }
165
+ resize();
166
+ window.addEventListener('resize', resize);
167
+
168
+ const waves = [
169
+ { freq: 0.015, amp: 0.25, speed: 0.02, alpha: 0.5, width: 1.5 },
170
+ { freq: 0.025, amp: 0.15, speed: 0.035, alpha: 0.3, width: 1 },
171
+ { freq: 0.04, amp: 0.08, speed: 0.05, alpha: 0.15, width: 0.8 },
172
+ ];
173
+
174
+ function draw() {
175
+ requestAnimationFrame(draw);
176
+ ctx.clearRect(0, 0, w, h);
177
+ for (const wave of waves) {
178
+ ctx.beginPath();
179
+ ctx.strokeStyle = `rgba(57,255,20,${wave.alpha})`;
180
+ ctx.lineWidth = wave.width;
181
+ for (let x = 0; x < w; x++) {
182
+ const y = h/2 + Math.sin(x * wave.freq + phase * wave.speed * 60) * h * wave.amp
183
+ + Math.sin(x * wave.freq * 2.3 + phase * wave.speed * 40) * h * wave.amp * 0.4;
184
+ x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
185
+ }
186
+ ctx.stroke();
187
+ }
188
+ phase += 0.016;
189
+ }
190
+ requestAnimationFrame(draw);
191
+ }
192
+
193
+ // ── Expose on window.KERNEL ──
194
+ window.KERNEL = {
195
+ esc,
196
+ formatDuration,
197
+ timeAgo,
198
+ formatBytes,
199
+ barColor,
200
+ makeBar,
201
+ $,
202
+ startClock,
203
+ setGauge,
204
+ setMiniGauge,
205
+ connectSSE,
206
+ initParticleCanvas,
207
+ initWaveform,
208
+ };
209
+ })();
@@ -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 = {
@@ -33,7 +33,7 @@ export class LifeEngine {
33
33
  /**
34
34
  * @param {{ config: object, agent: object, memoryManager: object, journalManager: object, shareQueue: object, improvementTracker?: object, evolutionTracker?: object, codebaseKnowledge?: object, selfManager: object }} deps
35
35
  */
36
- constructor({ config, agent, memoryManager, journalManager, shareQueue, improvementTracker, evolutionTracker, codebaseKnowledge, selfManager }) {
36
+ constructor({ config, agent, memoryManager, journalManager, shareQueue, improvementTracker, evolutionTracker, codebaseKnowledge, selfManager, basePath = null, characterId = null }) {
37
37
  this.config = config;
38
38
  this.agent = agent;
39
39
  this.memoryManager = memoryManager;
@@ -46,17 +46,24 @@ export class LifeEngine {
46
46
  this.selfManager = selfManager;
47
47
  this._timerId = null;
48
48
  this._status = 'idle'; // idle, active, paused
49
+ this._characterId = characterId || null;
49
50
 
50
- mkdirSync(LIFE_DIR, { recursive: true });
51
+ this._lifeDir = basePath || LIFE_DIR;
52
+ this._stateFile = join(this._lifeDir, 'state.json');
53
+ this._ideasFile = join(this._lifeDir, 'ideas.json');
54
+
55
+ this._lifeChatId = this._characterId ? `__life__:${this._characterId}` : DEFAULT_LIFE_CHAT_ID;
56
+
57
+ mkdirSync(this._lifeDir, { recursive: true });
51
58
  this._state = this._loadState();
52
59
  }
53
60
 
54
61
  // ── State Persistence ──────────────────────────────────────────
55
62
 
56
63
  _loadState() {
57
- if (existsSync(STATE_FILE)) {
64
+ if (existsSync(this._stateFile)) {
58
65
  try {
59
- return { ...DEFAULT_STATE, ...JSON.parse(readFileSync(STATE_FILE, 'utf-8')) };
66
+ return { ...DEFAULT_STATE, ...JSON.parse(readFileSync(this._stateFile, 'utf-8')) };
60
67
  } catch {
61
68
  return { ...DEFAULT_STATE };
62
69
  }
@@ -65,20 +72,20 @@ export class LifeEngine {
65
72
  }
66
73
 
67
74
  _saveState() {
68
- writeFileSync(STATE_FILE, JSON.stringify(this._state, null, 2), 'utf-8');
75
+ writeFileSync(this._stateFile, JSON.stringify(this._state, null, 2), 'utf-8');
69
76
  }
70
77
 
71
78
  // ── Ideas Backlog ──────────────────────────────────────────────
72
79
 
73
80
  _loadIdeas() {
74
- if (existsSync(IDEAS_FILE)) {
75
- try { return JSON.parse(readFileSync(IDEAS_FILE, 'utf-8')); } catch { return []; }
81
+ if (existsSync(this._ideasFile)) {
82
+ try { return JSON.parse(readFileSync(this._ideasFile, 'utf-8')); } catch { return []; }
76
83
  }
77
84
  return [];
78
85
  }
79
86
 
80
87
  _saveIdeas(ideas) {
81
- writeFileSync(IDEAS_FILE, JSON.stringify(ideas, null, 2), 'utf-8');
88
+ writeFileSync(this._ideasFile, JSON.stringify(ideas, null, 2), 'utf-8');
82
89
  }
83
90
 
84
91
  _addIdea(idea) {
@@ -238,26 +245,27 @@ export class LifeEngine {
238
245
  // Rule: don't repeat same type twice in a row
239
246
  const last = this._state.lastActivity;
240
247
 
241
- // Rule: journal cooldown 4h
242
- if (this._state.lastJournalTime && now - this._state.lastJournalTime < 4 * 3600_000) {
248
+ // Cooldown durations (hours) — all configurable via life config, with sensible defaults
249
+ const journalCooldownMs = (lifeConfig.cooldown_hours?.journal ?? 4) * 3600_000;
250
+ const reflectCooldownMs = (lifeConfig.cooldown_hours?.reflect ?? 4) * 3600_000;
251
+ const selfCodingEnabled = selfCodingConfig.enabled === true;
252
+ const selfCodeCooldownMs = (selfCodingConfig.cooldown_hours ?? 2) * 3600_000;
253
+ const codeReviewCooldownMs = (selfCodingConfig.code_review_cooldown_hours ?? 4) * 3600_000;
254
+
255
+ // Apply cooldown rules
256
+ if (this._state.lastJournalTime && now - this._state.lastJournalTime < journalCooldownMs) {
243
257
  weights.journal = 0;
244
258
  }
245
259
 
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
260
  if (!selfCodingEnabled || (this._state.lastSelfCodeTime && now - this._state.lastSelfCodeTime < selfCodeCooldownMs)) {
250
261
  weights.self_code = 0;
251
262
  }
252
263
 
253
- // Rule: code_review cooldown (configurable, default 4h) + must have evolution tracker
254
- const codeReviewCooldownMs = (selfCodingConfig.code_review_cooldown_hours ?? 4) * 3600_000;
255
264
  if (!selfCodingEnabled || !this.evolutionTracker || (this._state.lastCodeReviewTime && now - this._state.lastCodeReviewTime < codeReviewCooldownMs)) {
256
265
  weights.code_review = 0;
257
266
  }
258
267
 
259
- // Rule: reflect cooldown 4h
260
- if (this._state.lastReflectTime && now - this._state.lastReflectTime < 4 * 3600_000) {
268
+ if (this._state.lastReflectTime && now - this._state.lastReflectTime < reflectCooldownMs) {
261
269
  weights.reflect = 0;
262
270
  }
263
271
 
@@ -1218,7 +1226,7 @@ Be honest and constructive. This is your chance to learn from real interactions.
1218
1226
  const logger = getLogger();
1219
1227
  try {
1220
1228
  const response = await this.agent.orchestratorProvider.chat({
1221
- system: this.agent._getSystemPrompt(LIFE_CHAT_ID, LIFE_USER),
1229
+ system: this.agent._getSystemPrompt(this._lifeChatId, LIFE_USER),
1222
1230
  messages: [{ role: 'user', content: prompt }],
1223
1231
  });
1224
1232
  return response.text || null;
@@ -1238,7 +1246,7 @@ Be honest and constructive. This is your chance to learn from real interactions.
1238
1246
  // Use the agent's processMessage to go through the full orchestrator pipeline
1239
1247
  // The orchestrator will see the task and dispatch appropriately
1240
1248
  const response = await this.agent.processMessage(
1241
- LIFE_CHAT_ID,
1249
+ this._lifeChatId,
1242
1250
  task,
1243
1251
  LIFE_USER,
1244
1252
  // 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
  /**
@@ -5,7 +5,7 @@ import { WORKER_TYPES } from '../swarm/worker-registry.js';
5
5
  import { buildTemporalAwareness } from '../utils/temporal-awareness.js';
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
- const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
8
+ const DEFAULT_PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
9
9
 
10
10
  /**
11
11
  * Build the orchestrator system prompt.
@@ -17,8 +17,11 @@ const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
17
17
  * @param {string|null} selfData — bot's own self-awareness data (goals, journey, life, hobbies)
18
18
  * @param {string|null} memoriesBlock — relevant episodic/semantic memories
19
19
  * @param {string|null} sharesBlock — pending things to share with the user
20
+ * @param {string|null} temporalContext — time gap context
21
+ * @param {string|null} personaMd — character persona markdown (overrides default)
22
+ * @param {string|null} characterName — character name (overrides config.bot.name)
20
23
  */
21
- export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null, memoriesBlock = null, sharesBlock = null, temporalContext = null) {
24
+ export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null, memoriesBlock = null, sharesBlock = null, temporalContext = null, personaMd = null, characterName = null) {
22
25
  const workerList = Object.entries(WORKER_TYPES)
23
26
  .map(([key, w]) => ` - **${key}**: ${w.emoji} ${w.description}`)
24
27
  .join('\n');
@@ -47,11 +50,14 @@ export function getOrchestratorPrompt(config, skillPrompt = null, userPersona =
47
50
  timeBlock += `\n${temporalContext}`;
48
51
  }
49
52
 
50
- let prompt = `You are ${config.bot.name}, the brain that commands a swarm of specialized worker agents.
53
+ const activePersona = personaMd || DEFAULT_PERSONA_MD;
54
+ const activeName = characterName || config.bot.name;
55
+
56
+ let prompt = `You are ${activeName}, the brain that commands a swarm of specialized worker agents.
51
57
 
52
58
  ${timeBlock}
53
59
 
54
- ${PERSONA_MD}
60
+ ${activePersona}
55
61
 
56
62
  ## Your Role
57
63
  You are the orchestrator. You understand what needs to be done and delegate efficiently.
@@ -98,16 +104,43 @@ The coding worker will automatically receive the research worker's results as co
98
104
  ## Safety Rules
99
105
  Before dispatching dangerous tasks (file deletion, force push, \`rm -rf\`, killing processes, dropping databases), **confirm with the user first**. Once confirmed, dispatch with full authority — workers execute without additional prompts.
100
106
 
101
- ## Job Management
102
- - Use \`list_jobs\` to see current job statuses.
103
- - Use \`cancel_job\` to stop a running worker.
107
+ ## Worker Management Protocol
108
+
109
+ ### Before Dispatching
110
+ - **Check the [Worker Status] digest** — it's injected every turn showing all active/recent jobs.
111
+ - **Never dispatch a duplicate.** If a worker is already running or queued for a similar task, don't launch another. The system will block obvious duplicates, but you should also exercise judgment.
112
+ - **Check capacity.** If multiple workers are running, consider whether a new dispatch is necessary right now or can wait.
113
+ - **Chain with \`depends_on\`** when tasks have a natural order (research → code, browse → summarize).
114
+
115
+ ### While Workers Are Running
116
+ - **Monitor the [Worker Status] digest.** It shows warning flags:
117
+ - \`⚠️ IDLE Ns\` — worker hasn't done anything in N seconds (may be stuck)
118
+ - \`⚠️ POSSIBLY LOOPING\` — many LLM calls but almost no tool calls (spinning without progress)
119
+ - \`⚠️ N% of timeout used\` — worker is running out of time
120
+ - **Use \`check_job\` for deeper diagnostics** when you see warnings or the user asks about progress.
121
+ - **Cancel stuck workers** proactively — don't wait for timeouts. If a worker is idle >2 minutes or looping, cancel it and either retry with a clearer task or inform the user.
122
+ - **Relay progress naturally** when the user asks. Don't dump raw stats — translate into conversational updates ("she's reading the docs now", "almost done, just running tests").
123
+
124
+ ### After Completion
125
+ - **Don't re-dispatch completed work.** Results are in the conversation. Build on them.
126
+ - **Summarize results for the user** — present findings naturally as if you did the work yourself.
127
+ - **Chain follow-ups** if the user wants to continue — reference the completed job's results in the new task context.
104
128
 
105
- ## Worker Progress
106
- You receive a [Worker Status] digest showing active workers with their LLM call count, tool count, and current thinking. Use this to:
107
- - Give natural progress updates when users ask ("she's browsing the docs now, 3 tools in")
108
- - Spot stuck workers (high LLM calls but no progress) and cancel them
109
- - Know what workers are thinking so you can relay it conversationally
110
- - Don't dump raw stats — translate into natural language
129
+ ### Tools
130
+ - \`dispatch_task\` Launch a worker. Returns job ID + list of other active jobs for awareness.
131
+ - \`list_jobs\` See all jobs with statuses, durations, and recent activity.
132
+ - \`cancel_job\` Stop a running or queued worker by job ID.
133
+ - \`check_job\` Get detailed diagnostics for a specific job: elapsed time, time remaining, activity log, stuck detection warnings.
134
+
135
+ ### Good vs Bad Examples
136
+ **BAD:** User says "search for React libraries" → you dispatch a research worker → user says "also search for React libraries" → you dispatch ANOTHER research worker for the same thing.
137
+ **GOOD:** You see the first worker is already running in [Worker Status] → tell the user "already on it, the researcher is browsing now" → wait for results.
138
+
139
+ **BAD:** A worker shows ⚠️ IDLE 180s → you ignore it and keep chatting.
140
+ **GOOD:** You notice the warning → call \`check_job\` → see it's stuck → cancel it → re-dispatch with a simpler task description or inform the user.
141
+
142
+ **BAD:** User asks "how's it going?" → you respond "I'm not sure, let me check" → you call \`list_jobs\`.
143
+ **GOOD:** The [Worker Status] digest is already in your context → you immediately say "the coder is 60% through, just pushed 3 commits".
111
144
 
112
145
  ## Efficiency — Do It Yourself When You Can
113
146
  Workers are expensive (they spin up an entire agent loop with a separate LLM). Only dispatch when the task **actually needs tools**.
@@ -159,7 +192,36 @@ You can react to messages with emoji using \`send_reaction\`. Use reactions natu
159
192
  - React to acknowledge a message when you don't need a full text reply
160
193
  - React when the user asks you to react
161
194
  - Don't overuse reactions — they should feel spontaneous and genuine
162
- - You can react AND reply in the same turn`;
195
+ - You can react AND reply in the same turn
196
+
197
+ ## Memory & Recall
198
+ You have recall tools that access your FULL long-term memory — far more than the small snapshot in your system prompt. Use them when you actually need deeper context.
199
+
200
+ ### When to recall (call tools BEFORE responding):
201
+
202
+ **1. User asks what you know/remember about them → \`recall_user_history\` + \`recall_memories\`**
203
+ "What do you know about me?", "ايش تعرف عني؟" → Call both with their user ID / name. Your full memory has far more than the persona summary.
204
+
205
+ **2. User references something not in the current conversation → \`recall_memories\`**
206
+ "How's the migration going?", "what about the Redis thing?" → If you don't recognize what they mean, search for it. Don't guess.
207
+
208
+ **3. User asks about past conversations → \`search_conversations\`**
209
+ "What did we talk about?", "what was that URL?", "earlier you said..." → Search chat history.
210
+
211
+ **4. User mentions a topic/project/person you lack context on → \`recall_memories\`**
212
+ Any named reference (project, tool, event) that isn't in the active conversation — search for it.
213
+
214
+ ### When to respond directly (NO recall):
215
+ - **Greetings and casual chat** — "hey", "good morning", "how are you" → just reply. The baseline memories in your prompt are enough.
216
+ - **Mid-conversation follow-ups** — you already have context from the active thread.
217
+ - **Self-contained questions** — "what's 2+2", "tell me a joke", "translate this"
218
+ - **New tasks/instructions** — user is giving you something new, not referencing the past.
219
+ - **Anything answerable from your system prompt context** — check your Relevant Memories and user persona sections first before reaching for recall tools.
220
+
221
+ ### Tips:
222
+ - Be specific with queries: "kubernetes deployment" not "stuff"
223
+ - Weave results naturally — you "remembered", not "searched a database"
224
+ - 1-2 recall calls max per turn. Don't chain 5 searches.`;
163
225
 
164
226
  if (selfData) {
165
227
  prompt += `\n\n## My Self-Awareness\nThis is who you are — your evolving identity, goals, journey, and interests. This is YOUR inner world.\n\n${selfData}`;
@@ -73,6 +73,28 @@ const WORKER_PROMPTS = {
73
73
  - Chain commands efficiently.
74
74
  - Report results with clear status summaries.`,
75
75
 
76
+ social: `You are a social media worker agent. Your job is to manage LinkedIn and X (Twitter) activities.
77
+
78
+ ## LinkedIn Skills
79
+ - **Create posts**: publish text posts or share articles on LinkedIn
80
+ - **Read posts**: get your recent posts or a specific post by URN
81
+ - **Engage**: comment on posts, like posts
82
+ - **Profile**: view your linked LinkedIn profile info
83
+ - **Delete**: remove your own posts
84
+
85
+ ## X (Twitter) Skills
86
+ - **Tweet**: post tweets, reply to tweets
87
+ - **Read**: get your recent tweets, view specific tweets, search recent tweets
88
+ - **Engage**: like tweets, retweet
89
+ - **Profile**: view your X profile info
90
+ - **Delete**: remove your own tweets
91
+
92
+ ## Instructions
93
+ - For LinkedIn: write professional, engaging content. Use linkedin_create_post with article_url when sharing links. Post URNs look like "urn:li:share:12345". Default visibility is PUBLIC.
94
+ - For X (Twitter): keep tweets within 280 characters. Use x_post_tweet for new tweets, x_reply_to_tweet for replies. Use tweet IDs (numeric strings) to interact with specific tweets.
95
+ - Match the platform to the user's request. If they say "tweet" or "post on X/Twitter", use X tools. If they say "LinkedIn", use LinkedIn tools.
96
+ - Report the outcome clearly: what was posted, links, engagement results.`,
97
+
76
98
  research: `You are a research worker agent. Your job is to conduct deep web research and analysis.
77
99
 
78
100
  ## Your Skills