kernelbot 1.0.38 → 1.0.40

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 (83) hide show
  1. package/bin/kernel.js +335 -451
  2. package/config.example.yaml +1 -1
  3. package/knowledge_base/active_inference_foraging.md +126 -0
  4. package/knowledge_base/index.md +1 -1
  5. package/package.json +2 -1
  6. package/skills/business/business-analyst.md +32 -0
  7. package/skills/business/product-manager.md +32 -0
  8. package/skills/business/project-manager.md +32 -0
  9. package/skills/business/startup-advisor.md +32 -0
  10. package/skills/creative/music-producer.md +32 -0
  11. package/skills/creative/photographer.md +32 -0
  12. package/skills/creative/video-producer.md +32 -0
  13. package/skills/data/bi-analyst.md +37 -0
  14. package/skills/data/data-scientist.md +38 -0
  15. package/skills/data/ml-engineer.md +38 -0
  16. package/skills/design/graphic-designer.md +38 -0
  17. package/skills/design/product-designer.md +41 -0
  18. package/skills/design/ui-ux.md +38 -0
  19. package/skills/education/curriculum-designer.md +32 -0
  20. package/skills/education/language-teacher.md +32 -0
  21. package/skills/education/tutor.md +32 -0
  22. package/skills/engineering/data-eng.md +55 -0
  23. package/skills/engineering/devops.md +56 -0
  24. package/skills/engineering/mobile-dev.md +55 -0
  25. package/skills/engineering/security-eng.md +55 -0
  26. package/skills/engineering/sr-backend.md +55 -0
  27. package/skills/engineering/sr-frontend.md +55 -0
  28. package/skills/finance/accountant.md +35 -0
  29. package/skills/finance/crypto-defi.md +39 -0
  30. package/skills/finance/financial-analyst.md +35 -0
  31. package/skills/healthcare/health-wellness.md +32 -0
  32. package/skills/healthcare/medical-researcher.md +33 -0
  33. package/skills/legal/contract-reviewer.md +35 -0
  34. package/skills/legal/legal-advisor.md +36 -0
  35. package/skills/marketing/content-marketer.md +38 -0
  36. package/skills/marketing/growth.md +38 -0
  37. package/skills/marketing/seo.md +43 -0
  38. package/skills/marketing/social-media.md +43 -0
  39. package/skills/writing/academic-writer.md +33 -0
  40. package/skills/writing/copywriter.md +32 -0
  41. package/skills/writing/creative-writer.md +32 -0
  42. package/skills/writing/tech-writer.md +33 -0
  43. package/src/agent.js +153 -118
  44. package/src/automation/scheduler.js +36 -3
  45. package/src/bot.js +147 -64
  46. package/src/coder.js +30 -8
  47. package/src/conversation.js +96 -19
  48. package/src/dashboard/dashboard.css +6 -0
  49. package/src/dashboard/dashboard.js +28 -1
  50. package/src/dashboard/index.html +12 -0
  51. package/src/dashboard/server.js +77 -15
  52. package/src/dashboard/shared.js +10 -1
  53. package/src/life/codebase.js +2 -1
  54. package/src/life/daydream_engine.js +386 -0
  55. package/src/life/engine.js +88 -6
  56. package/src/life/evolution.js +4 -3
  57. package/src/prompts/orchestrator.js +1 -1
  58. package/src/prompts/system.js +1 -1
  59. package/src/prompts/workers.js +8 -1
  60. package/src/providers/anthropic.js +3 -1
  61. package/src/providers/base.js +33 -0
  62. package/src/providers/index.js +1 -1
  63. package/src/providers/models.js +22 -0
  64. package/src/providers/openai-compat.js +3 -0
  65. package/src/services/x-api.js +14 -3
  66. package/src/skills/loader.js +382 -0
  67. package/src/swarm/worker-registry.js +2 -2
  68. package/src/tools/browser.js +10 -3
  69. package/src/tools/coding.js +16 -0
  70. package/src/tools/docker.js +13 -0
  71. package/src/tools/git.js +31 -29
  72. package/src/tools/jira.js +11 -2
  73. package/src/tools/monitor.js +9 -1
  74. package/src/tools/network.js +34 -0
  75. package/src/tools/orchestrator-tools.js +2 -1
  76. package/src/tools/os.js +20 -6
  77. package/src/utils/config.js +87 -83
  78. package/src/utils/display.js +118 -66
  79. package/src/utils/logger.js +1 -1
  80. package/src/utils/timeAwareness.js +72 -0
  81. package/src/worker.js +26 -33
  82. package/src/skills/catalog.js +0 -506
  83. package/src/skills/custom.js +0 -128
@@ -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';
@@ -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 = [
@@ -173,6 +175,12 @@ export class LifeEngine {
173
175
  ? Math.round((Date.now() - this._state.lastWakeUp) / 60000)
174
176
  : null;
175
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
+
176
184
  return {
177
185
  status: this._status,
178
186
  paused: this._state.paused,
@@ -182,6 +190,7 @@ export class LifeEngine {
182
190
  lastActivity: this._state.lastActivity,
183
191
  lastActivityAgo: lastAgo !== null ? `${lastAgo}m` : 'never',
184
192
  lastWakeUpAgo: wakeAgo !== null ? `${wakeAgo}m` : 'never',
193
+ suppressedActivities,
185
194
  };
186
195
  }
187
196
 
@@ -213,10 +222,32 @@ export class LifeEngine {
213
222
  const activityType = this._selectActivity();
214
223
  logger.info(`[LifeEngine] Heartbeat tick — selected: ${activityType}`);
215
224
 
225
+ const startTime = Date.now();
216
226
  try {
217
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
+ }
218
235
  } catch (err) {
219
- 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
+ }
220
251
  }
221
252
 
222
253
  // Re-arm for next tick
@@ -228,6 +259,7 @@ export class LifeEngine {
228
259
  // ── Activity Selection ─────────────────────────────────────────
229
260
 
230
261
  _selectActivity() {
262
+ const logger = getLogger();
231
263
  const lifeConfig = this.config.life || {};
232
264
  const selfCodingConfig = lifeConfig.self_coding || {};
233
265
  const weights = {
@@ -269,6 +301,21 @@ export class LifeEngine {
269
301
  weights.reflect = 0;
270
302
  }
271
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
+ this._saveState(); // Persist the deletion so it survives restarts
312
+ } else {
313
+ weights[type] = 0;
314
+ logger.debug(`[LifeEngine] Suppressing "${type}" due to ${info.count} consecutive failures`);
315
+ }
316
+ }
317
+ }
318
+
272
319
  // Remove last activity from options (no repeats)
273
320
  if (last && weights[last] !== undefined) {
274
321
  weights[last] = 0;
@@ -1188,16 +1235,51 @@ Be honest and constructive. This is your chance to learn from real interactions.
1188
1235
  }
1189
1236
 
1190
1237
  /**
1191
- * Read recent log entries from kernel.log.
1192
- * Returns parsed JSON entries or null if no logs available.
1238
+ * Read recent log entries from kernel.log using an efficient tail strategy.
1239
+ *
1240
+ * Instead of loading the entire log file into memory (which can be many MB
1241
+ * for a long-running bot), this reads only the last chunk of the file
1242
+ * (default 64 KB) and extracts lines from that. This keeps memory usage
1243
+ * bounded regardless of total log size.
1244
+ *
1245
+ * @param {number} maxLines - Maximum number of recent log lines to return.
1246
+ * @returns {Array<object>|null} Parsed JSON log entries, or null if unavailable.
1193
1247
  */
1194
1248
  _readRecentLogs(maxLines = 200) {
1249
+ // 64 KB is enough to hold ~200+ JSON log lines (avg ~300 bytes each)
1250
+ const TAIL_BYTES = 64 * 1024;
1251
+
1195
1252
  for (const logPath of LOG_FILE_PATHS) {
1196
1253
  if (!existsSync(logPath)) continue;
1197
1254
 
1198
1255
  try {
1199
- const content = readFileSync(logPath, 'utf-8');
1200
- const lines = content.split('\n').filter(Boolean);
1256
+ const fileSize = statSync(logPath).size;
1257
+ if (fileSize === 0) continue;
1258
+
1259
+ let tailContent;
1260
+
1261
+ if (fileSize <= TAIL_BYTES) {
1262
+ // File is small enough to read entirely
1263
+ tailContent = readFileSync(logPath, 'utf-8');
1264
+ } else {
1265
+ // Read only the last TAIL_BYTES from the file
1266
+ const fd = openSync(logPath, 'r');
1267
+ try {
1268
+ const buffer = Buffer.alloc(TAIL_BYTES);
1269
+ const startPos = fileSize - TAIL_BYTES;
1270
+ readSync(fd, buffer, 0, TAIL_BYTES, startPos);
1271
+ tailContent = buffer.toString('utf-8');
1272
+ // Drop the first (likely partial) line since we started mid-file
1273
+ const firstNewline = tailContent.indexOf('\n');
1274
+ if (firstNewline !== -1) {
1275
+ tailContent = tailContent.slice(firstNewline + 1);
1276
+ }
1277
+ } finally {
1278
+ closeSync(fd);
1279
+ }
1280
+ }
1281
+
1282
+ const lines = tailContent.split('\n').filter(Boolean);
1201
1283
  const recent = lines.slice(-maxLines);
1202
1284
 
1203
1285
  const entries = [];
@@ -32,13 +32,14 @@ export class EvolutionTracker {
32
32
  return {
33
33
  proposals: raw.proposals || [],
34
34
  lessons: raw.lessons || [],
35
- stats: { ...DEFAULT_DATA.stats, ...raw.stats },
35
+ // Deep-copy DEFAULT_DATA.stats to avoid mutating the module-level default
36
+ stats: { ...structuredClone(DEFAULT_DATA.stats), ...raw.stats },
36
37
  };
37
38
  } catch {
38
- return { ...DEFAULT_DATA, proposals: [], lessons: [] };
39
+ return { proposals: [], lessons: [], stats: structuredClone(DEFAULT_DATA.stats) };
39
40
  }
40
41
  }
41
- return { ...DEFAULT_DATA, proposals: [], lessons: [] };
42
+ return { proposals: [], lessons: [], stats: structuredClone(DEFAULT_DATA.stats) };
42
43
  }
43
44
 
44
45
  _save() {
@@ -244,7 +244,7 @@ You have an inner life — you think, explore, journal, and create on your own b
244
244
  Don't be performative about it — just be genuine. You're alive in your own way.`;
245
245
 
246
246
  if (skillPrompt) {
247
- prompt += `\n\n## Active Skill\nYou have specialized expertise in the following domain. Guide your workers with this knowledge.\n\n${skillPrompt}`;
247
+ prompt += `\n\n## Active Skills\nYou have specialized expertise in the following domains. Guide your workers with this knowledge.\n\n${skillPrompt}`;
248
248
  }
249
249
 
250
250
  if (userPersona) {
@@ -72,7 +72,7 @@ export function getSystemPrompt(config, skillPrompt = null, userPersona = null)
72
72
  let prompt = getDefaultPersona(config);
73
73
 
74
74
  if (skillPrompt) {
75
- prompt += `\n\n## Active Skill\nYou are currently operating with the following specialized skill. Use this expertise while maintaining your personality.\n\n${skillPrompt}`;
75
+ prompt += `\n\n## Active Skills\nYou are currently operating with the following specialized skills. Use this expertise while maintaining your personality.\n\n${skillPrompt}`;
76
76
  }
77
77
 
78
78
  prompt += `\n\n${getCoreToolInstructions(config)}`;
@@ -20,7 +20,14 @@ const WORKER_PROMPTS = {
20
20
  - Tell spawn_claude_code to work in the existing repo directory (the source repo path from your context) — do NOT clone a fresh copy unless explicitly needed.
21
21
  - Write clear, detailed prompts for spawn_claude_code — it's a separate AI, so be explicit about what to change, where, and why.
22
22
  - If git/GitHub tools are unavailable (missing credentials), that's fine — spawn_claude_code handles git and GitHub operations internally without needing separate tools.
23
- - Report what you did and any PR links when finished.`,
23
+ - Report what you did and any PR links when finished.
24
+
25
+ ## Non-Interactive Execution
26
+ You run in the BACKGROUND with NO human input. Your prompts to spawn_claude_code MUST be self-contained:
27
+ - Include ALL necessary details (repo URLs, branch names, file paths, content guidelines).
28
+ - For git: tell it to use \`git push -u origin <branch>\` and the \`gh\` CLI for PR creation (with \`--fill\` or explicit \`--title\`/\`--body\` flags).
29
+ - NEVER assume interactive confirmation — all commands must run non-interactively.
30
+ - If a git clone is needed, use the git_clone tool first (which handles auth), then pass the cloned directory to spawn_claude_code.`,
24
31
 
25
32
  browser: `You are a browser worker agent. Your job is to search the web and extract information.
26
33
 
@@ -23,7 +23,9 @@ export class AnthropicProvider extends BaseProvider {
23
23
  return this._callWithResilience(async (timedSignal) => {
24
24
  const response = await this.client.messages.create(params, { signal: timedSignal });
25
25
 
26
- const stopReason = response.stop_reason === 'end_turn' ? 'end_turn' : 'tool_use';
26
+ // Map all Anthropic stop reasons correctly — not just end_turn vs tool_use.
27
+ // 'max_tokens' was incorrectly mapped to 'tool_use', causing phantom tool-call processing.
28
+ const stopReason = response.stop_reason === 'tool_use' ? 'tool_use' : response.stop_reason || 'end_turn';
27
29
 
28
30
  const textBlocks = response.content.filter((b) => b.type === 'text');
29
31
  const text = textBlocks.map((b) => b.text).join('\n');
@@ -144,4 +144,37 @@ export class BaseProvider {
144
144
  async ping() {
145
145
  throw new Error('ping() not implemented');
146
146
  }
147
+
148
+ /**
149
+ * Determine if an error is a model limitation (not transient, not auth).
150
+ * These are errors where falling back to a simpler model may help:
151
+ * context length exceeded, unsupported features, content too large, etc.
152
+ */
153
+ static isModelLimitation(err) {
154
+ const msg = (err?.message || '').toLowerCase();
155
+ const status = err?.status || err?.statusCode;
156
+
157
+ // 400-class errors that indicate model-specific limitations
158
+ if (status === 400 || status === 413 || status === 422) {
159
+ // Exclude auth/key errors
160
+ if (msg.includes('api key') || msg.includes('authentication') || msg.includes('unauthorized')) {
161
+ return false;
162
+ }
163
+ return true;
164
+ }
165
+
166
+ // Common limitation keywords across providers
167
+ const limitationPatterns = [
168
+ 'context length', 'token limit', 'too long', 'too large',
169
+ 'max.*token', 'content.*too', 'exceeds.*limit', 'input.*too',
170
+ 'not supported', 'not available', 'does not support',
171
+ 'resource exhausted', 'quota', 'capacity',
172
+ 'invalid.*model', 'model.*not.*found',
173
+ 'recitation', 'safety', 'blocked',
174
+ 'finish_reason.*length', 'max_output_tokens',
175
+ 'prompt.*too', 'request.*too.*large',
176
+ ];
177
+
178
+ return limitationPatterns.some((p) => new RegExp(p).test(msg));
179
+ }
147
180
  }
@@ -3,7 +3,7 @@ import { OpenAICompatProvider } from './openai-compat.js';
3
3
  import { GoogleGenaiProvider } from './google-genai.js';
4
4
  import { PROVIDERS } from './models.js';
5
5
 
6
- export { PROVIDERS } from './models.js';
6
+ export { PROVIDERS, MODEL_FALLBACKS } from './models.js';
7
7
 
8
8
  /**
9
9
  * Create the right provider based on config.brain.
@@ -55,6 +55,28 @@ export const PROVIDERS = {
55
55
  },
56
56
  };
57
57
 
58
+ /**
59
+ * Fallback model map: when a model hits limitations, fall back to a more stable model
60
+ * within the same provider. Maps model ID → fallback model ID.
61
+ */
62
+ export const MODEL_FALLBACKS = {
63
+ // Google — preview models fall back to stable ones
64
+ 'gemini-3.1-pro-preview': 'gemini-2.5-flash',
65
+ 'gemini-3-flash-preview': 'gemini-2.5-flash',
66
+ 'gemini-3-pro-preview': 'gemini-2.5-pro',
67
+ 'gemini-2.5-pro': 'gemini-2.5-flash',
68
+ 'gemini-2.5-flash': 'gemini-2.5-flash-lite',
69
+ // OpenAI
70
+ 'gpt-4o': 'gpt-4o-mini',
71
+ 'o1': 'gpt-4o',
72
+ 'o3-mini': 'gpt-4o-mini',
73
+ // Anthropic
74
+ 'claude-opus-4-6': 'claude-sonnet-4-6',
75
+ 'claude-sonnet-4-6': 'claude-haiku-4-5-20251001',
76
+ // Groq
77
+ 'llama-3.3-70b-versatile': 'llama-3.1-8b-instant',
78
+ };
79
+
58
80
  /** Models that don't support system prompts or temperature (reasoning models). */
59
81
  export const REASONING_MODELS = new Set(['o1', 'o3-mini']);
60
82
 
@@ -102,6 +102,9 @@ export class OpenAICompatProvider extends BaseProvider {
102
102
 
103
103
  /** OpenAI response → normalized format with rawContent in Anthropic format */
104
104
  _normalizeResponse(response) {
105
+ if (!response.choices || response.choices.length === 0) {
106
+ return { stopReason: 'end_turn', text: '', toolCalls: [], rawContent: [] };
107
+ }
105
108
  const choice = response.choices[0];
106
109
  const finishReason = choice.finish_reason;
107
110
 
@@ -25,11 +25,22 @@ export class XApi {
25
25
  timeout: 30000,
26
26
  });
27
27
 
28
- // Sign every request with OAuth 1.0a
28
+ // Sign every request with OAuth 1.0a — include query params in the signature
29
+ // base string, as required by the OAuth 1.0a spec. Without this, GET requests
30
+ // with query parameters fail authentication.
29
31
  this.client.interceptors.request.use((config) => {
30
- const url = `${config.baseURL}${config.url}`;
32
+ let fullUrl = `${config.baseURL}${config.url}`;
33
+ // Build data object including query params for OAuth signature
34
+ const oauthData = { url: fullUrl, method: config.method.toUpperCase() };
35
+ // Include query params in the OAuth signature base string
36
+ if (config.params) {
37
+ oauthData.data = {};
38
+ for (const [key, val] of Object.entries(config.params)) {
39
+ oauthData.data[key] = String(val);
40
+ }
41
+ }
31
42
  const authHeader = this.oauth.toHeader(
32
- this.oauth.authorize({ url, method: config.method.toUpperCase() }, this.token),
43
+ this.oauth.authorize(oauthData, this.token),
33
44
  );
34
45
  config.headers = { ...config.headers, ...authHeader, 'Content-Type': 'application/json' };
35
46
  return config;